diff --git a/src/Entity/ClassDef.php b/src/Entity/ClassDef.php index 3b2dea2e..54d3215b 100644 --- a/src/Entity/ClassDef.php +++ b/src/Entity/ClassDef.php @@ -32,6 +32,7 @@ class ClassDef extends ClassLikeDef public string $extends = ''; public bool $requireCtor = false; public bool $enum = false; + public ?string $extensionProviderTarget = null; /** * Backing type for backed enums ('int' or 'string'), null for pure enums. diff --git a/src/Parser/UniversalMethodCall.php b/src/Parser/UniversalMethodCall.php index 4be57f5f..26039d35 100644 --- a/src/Parser/UniversalMethodCall.php +++ b/src/Parser/UniversalMethodCall.php @@ -13,6 +13,7 @@ use PhpParser\NodeAbstract; trait UniversalMethodCall { + private ?array $extensionProviderMethods = null; protected const array UNIVERSAL_METHODS = [ Type::INT => [ 'add' => ['handler' => 'calc_op', 'op' => '+', 'return_type' => Type::INT, 'min_args' => 1, 'max_args' => 1], @@ -337,35 +338,69 @@ trait UniversalMethodCall return $ext ? $ext['return_type'] : null; } - protected const array TYPE_EXTENSION_PREFIX = [ - Type::INT => 'int', - Type::FLOAT => 'float', - Type::BOOL => 'bool', - Type::STR => 'str', - Type::ARRAY => 'array', - Type::STREAM => 'stream', - Type::BIGINT => 'bigint', - Type::DECIMAL => 'decimal', - Type::BIGFLOAT => 'bigfloat', - Type::BOX => 'box', - ]; - - protected function extensionFunctionName(string $prefix, string $method): string + private function getExtensionProviderMethods(): array { - return $prefix . '_' . $method; + if ($this->extensionProviderMethods !== null) { + return $this->extensionProviderMethods; + } + $registry = []; + foreach ($this->symbols->classes() as $provider) { + $target = $provider->extensionProviderTarget; + if ($target === null) { + continue; + } + foreach ($provider->methods as $method) { + if (str_starts_with($method->name, '__')) { + continue; + } + if (!($method->flags & \PhpParser\Modifiers::PUBLIC)) { + continue; + } + if (!($method->flags & \PhpParser\Modifiers::STATIC)) { + $this->error("Extension provider method {$provider->getNamespacedName(false)}::{$method->name}() must be static"); + } + $function = $method->functionDef; + if ($function === null || empty($function->argInfoList)) { + $this->error("Extension provider method {$provider->getNamespacedName(false)}::{$method->name}() must declare a receiver parameter"); + } + $receiver = $function->argInfoList[0]; + if ($receiver->byRef || !$this->extensionReceiverMatchesTarget($receiver, $target)) { + $this->error("Invalid receiver parameter for extension provider method {$provider->getNamespacedName(false)}::{$method->name}()"); + } + $targetKey = strtolower(ltrim($target, '\\')); + $methodKey = strtolower($method->name); + if (isset($registry[$targetKey][$methodKey])) { + $this->error("Duplicate extension method {$target}::{$method->name}()"); + } + $registry[$targetKey][$methodKey] = [ + 'handler' => 'provider_extension', + 'fn' => $this->getNativeName($method->name, $provider->namespace, $provider->name), + 'class' => $provider->getNamespacedName(false), + 'return_type' => $function->returnType, + 'min_args' => max(0, $function->argCountRequired - 1), + 'max_args' => $function->hasVariadicArg() ? -1 : count($function->argInfoList) - 1, + ]; + } + } + return $this->extensionProviderMethods = $registry; } - protected function findUserExtensionFunction(string $prefix, string $method, string $namespace = ''): ?array + private function extensionReceiverMatchesTarget($receiver, string $target): bool { - $function = $this->getNativeName($this->extensionFunctionName($prefix, $method), $namespace); - if (!$this->hasFunction($function)) { - return null; - } - $definition = $this->getFunction($function); - if ($definition->namespace !== $namespace) { - return null; + $builtinTargets = [ + Type::VAR, Type::INT, Type::FLOAT, Type::BOOL, Type::STR, + Type::ARRAY, Type::OBJECT, Type::STREAM, Type::BIGINT, + Type::BIGFLOAT, Type::DECIMAL, Type::BOX, + ]; + if (!in_array($target, $builtinTargets, true)) { + return $receiver->type === Type::OBJECT && $this->isSameClassName($receiver->declaredClass, $target); } - return ['name' => $function, 'definition' => $definition]; + return $receiver->type === $target; + } + + private function findProviderExtension(string $target, string $method): ?array + { + return $this->getExtensionProviderMethods()[strtolower(ltrim($target, '\\'))][strtolower($method)] ?? null; } protected const array TO_CONVERT_FN = [ @@ -421,34 +456,7 @@ trait UniversalMethodCall 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); - - $extension = $this->findUserExtensionFunction($shortClass, $method, $namespace); - if ($extension === null) { - return null; - } - $function = $extension['name']; - $funcDef = $extension['definition']; - if (empty($funcDef->argInfoList)) { - return null; - } - $receiver = $funcDef->argInfoList[0]; - if ($receiver->byRef - || $receiver->type !== Type::OBJECT - || !$this->isSameClassName($receiver->declaredClass, $class)) { - return null; - } - - $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 $this->findProviderExtension($class, $method); } /** @@ -461,36 +469,7 @@ trait UniversalMethodCall */ protected function findExtensionMethod(string $type, string $method): ?array { - $prefix = self::TYPE_EXTENSION_PREFIX[$type] ?? null; - if ($prefix === null) { - return null; - } - - $funcName = $this->extensionFunctionName($prefix, $method); - $resolvedName = $this->resolveExtensionFunctionName($funcName); - if ($resolvedName !== null) { - $funcDef = $this->getFunction($resolvedName); - if ($funcDef->namespace !== '' || !$this->validateExtensionFirstParam($type, $funcDef)) { - return null; - } - return [ - 'handler' => 'php_fn', - 'fn' => $resolvedName, - 'receiver_pos' => 1, - 'return_type' => $funcDef->returnType, - 'min_args' => 0, - 'max_args' => -1, - ]; - } - - if ($this->isInternalFunction($funcName)) { - $internal = $this->buildInternalExtensionMethod($type, $funcName); - if ($internal !== null) { - return $internal; - } - } - - return null; + return $this->findProviderExtension($type, $method); } /** @@ -499,32 +478,7 @@ trait UniversalMethodCall */ protected function findKeywordExtensionMethod(string $method): ?array { - $extension = $this->findUserExtensionFunction('_', $method); - if ($extension === null) { - return null; - } - $funcName = $extension['name']; - $funcDef = $extension['definition']; - if (empty($funcDef->argInfoList)) { - return null; - } - $firstParam = $funcDef->argInfoList[0]; - if ($firstParam->type !== Type::VAR) { - return null; - } - - $totalParams = count($funcDef->argInfoList); - $minArgs = max(0, $funcDef->argCountRequired - 1); - $maxArgs = $funcDef->hasVariadicArg() ? -1 : $totalParams - 1; - - return [ - 'handler' => 'php_fn', - 'fn' => $funcName, - 'receiver_pos' => 1, - 'return_type' => $funcDef->returnType, - 'min_args' => $minArgs, - 'max_args' => $maxArgs, - ]; + return $this->findProviderExtension(Type::VAR, $method); } /** @@ -651,15 +605,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), + 'provider_extension' => $this->genProviderExtensionCall($receiver, $def, $expr->args), default => null, }; } - protected function genObjectExtensionFn(string $receiver, string $nativeFunc, array $args): string + protected function genProviderExtensionCall(string $receiver, array $definition, array $args): string { + $nativeFunc = $definition['fn']; $tail = $this->parseNativeCallArgs($args, $nativeFunc, 1); - return self::PREFIX . $nativeFunc . '(' . $receiver . ($tail === '' ? '' : ', ' . $tail) . ')'; + return self::PREFIX . $nativeFunc . '(' . $this->getCeWrapper($definition['class']) . ', ' + . $receiver . ($tail === '' ? '' : ', ' . $tail) . ')'; } /** @@ -678,6 +634,7 @@ trait UniversalMethodCall 'php_fn' => $this->genUniversalPhpFn('php::toStream(' . $streamVar . ')', $def['fn'], $expr->args, $def['receiver_pos'] ?? 0, $def['const_args'] ?? []), 'direct_method' => $this->genUniversalDirectMethod('php::toStream(' . $streamVar . ')', $def['method'], $expr->args, $def['int_cast_args'] ?? []), 'cpp_fn' => $this->genUniversalCppFn('php::toStream(' . $streamVar . ')', $def['fn'], $expr->args, $def['receiver_pos'] ?? 0), + 'provider_extension' => $this->genProviderExtensionCall('php::toStream(' . $streamVar . ')', $def, $expr->args), default => null, }; diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 0601e3e2..c6aca98a 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -524,6 +524,7 @@ class Preprocessor extends CompilerBase } $this->classDef = new ClassDef($this->class, $flags, $this->namespace); + $this->classDef->extensionProviderTarget = $this->parseExtensionProviderTarget($class); $this->addClass($fullClassName, $this->classDef); if (!empty($class->extends)) { @@ -602,6 +603,71 @@ class Preprocessor extends CompilerBase return $code; } + protected function parseExtensionProviderTarget(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): ?string + { + foreach ($class->attrGroups as $groupIndex => $group) { + foreach ($group->attrs as $attributeIndex => $attribute) { + $parts = $attribute->name->getParts(); + if (strtolower((string) end($parts)) !== 'extensionprovider') { + continue; + } + if (!$class instanceof Node\Stmt\Class_) { + $this->fatalError($class, 'ExtensionProvider can only be applied to classes'); + } + if (count($attribute->args) !== 1) { + $this->fatalError($attribute, 'ExtensionProvider expects exactly one target'); + } + $target = $this->parseExtensionProviderTargetValue($attribute->args[0]->value, $attribute); + unset($group->attrs[$attributeIndex]); + $group->attrs = array_values($group->attrs); + if (empty($group->attrs)) { + unset($class->attrGroups[$groupIndex]); + $class->attrGroups = array_values($class->attrGroups); + } + return $target; + } + } + return null; + } + + private function parseExtensionProviderTargetValue(Node\Expr $value, NodeAbstract $errorNode): string + { + if ($value instanceof Node\Expr\ClassConstFetch + && $this->isNameExpr($value->class) + && $this->isIdExpr($value->name)) { + $class = strtolower(ltrim($value->class->toString(), '\\')); + $constant = strtolower($value->name->toString()); + if ($constant === 'class') { + return $this->getNamespacedClassName($value->class->toString()); + } + $targets = [ + 'native_types' => [ + 'type_int' => Type::INT, + 'type_float' => Type::FLOAT, + 'type_bool' => Type::BOOL, + 'type_bigint' => Type::BIGINT, + 'type_bigfloat' => Type::BIGFLOAT, + 'type_decimal' => Type::DECIMAL, + ], + 'complex_types' => [ + 'type_any' => Type::VAR, + 'type_var' => Type::VAR, + 'type_variant' => Type::VAR, + 'type_str' => Type::STR, + 'type_string' => Type::STR, + 'type_array' => Type::ARRAY, + 'type_object' => Type::OBJECT, + 'type_stream' => Type::STREAM, + 'type_box' => Type::BOX, + ], + ]; + if (isset($targets[$class][$constant])) { + return $targets[$class][$constant]; + } + } + $this->fatalError($errorNode, 'ExtensionProvider target must use native_types, complex_types, or ClassName::class'); + } + protected function buildLiteralArrayInitPlan(Node\Expr\Array_ $defaultNode): ArrayInitPlan { $localVarCount = count($this->context->localVars); diff --git a/src/Translator.php b/src/Translator.php index 0055f24e..f9a99c8f 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2504,6 +2504,7 @@ CODE; $this->fatalError($class, "class {$fullName} not found"); } $this->classDef = $this->getClass($fullName); + $this->parseExtensionProviderTarget($class); // 如果不是继承自内置类,需要检查父类是否存在,在预处理阶段只需检查了是否继承内置类 // 目前不允许继承自动态加载的自定义类 diff --git a/src/gen_stub.php b/src/gen_stub.php index a882ef47..4dc9370b 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -3549,6 +3549,10 @@ class AttributeInfo { foreach ($attributeGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { + $parts = $attr->name->getParts(); + if (strtolower((string) end($parts)) === 'extensionprovider') { + continue; + } $attributes[] = new AttributeInfo($attr->name->toString(), $attr->args); } } diff --git a/src/polyfills.php b/src/polyfills.php index 65c625f6..56e8adba 100644 --- a/src/polyfills.php +++ b/src/polyfills.php @@ -6,6 +6,14 @@ * @contact service@swoole.com */ +#[Attribute(Attribute::TARGET_CLASS)] +final readonly class ExtensionProvider +{ + public function __construct(public string $target) + { + } +} + class native_types { public const type_int = 'int'; diff --git a/tests/compiler/keyword_extension/001.phpt b/tests/compiler/keyword_extension/001.phpt index 926aaeba..11d75ad9 100644 --- a/tests/compiler/keyword_extension/001.phpt +++ b/tests/compiler/keyword_extension/001.phpt @@ -1,12 +1,17 @@ --TEST-- -keyword extension method: exact snake_case name +Keyword ExtensionProvider method with snake_case name --FILE-- --EXPECTF-- -string(%d) "%s/aot/static/../../A" \ No newline at end of file +string(%d) "%s/compiler/static/../../A" diff --git a/tests/compiler/stream_method/extension.phpt b/tests/compiler/stream_method/extension.phpt index 7a89ab7a..840329c5 100644 --- a/tests/compiler/stream_method/extension.phpt +++ b/tests/compiler/stream_method/extension.phpt @@ -3,24 +3,28 @@ stream extension method support --FILE-- name . $suffix . ':snake'; - } - - function User_displayName(User $user): string - { - return strtoupper($user->name) . ':camel'; - } + public static function testMethod(User $user, string $suffix): string + { + return $user->name . $suffix . ':snake'; + } - function User_format_name(int $invalid): string - { - return 'invalid'; - } + public static function displayName(User $user): string + { + return strtoupper($user->name) . ':camel'; + } - function User_formatName(User $user): string - { - return '[' . $user->name . ']'; - } + public static function formatName(User $user): string + { + return '[' . $user->name . ']'; + } - function User_existing(User $user): string - { - return 'extension'; + public static function existing(User $user): string + { + return 'extension'; + } } } diff --git a/tests/compiler/universal_method/object_extension_exact_name.phpt b/tests/compiler/universal_method/object_extension_exact_name.phpt index 2036e5de..ef0fa5ac 100644 --- a/tests/compiler/universal_method/object_extension_exact_name.phpt +++ b/tests/compiler/universal_method/object_extension_exact_name.phpt @@ -1,5 +1,5 @@ --TEST-- -Object extension methods require consistent names and ignore letter case +Object ExtensionProvider methods require consistent names and ignore letter case --FILE-- name; - } - - function UserService_profile_label(UserService $service): string - { - return 'snake:' . $service->name; - } - - function user_service_wrongName(UserService $service): string - { - return 'wrong class prefix'; - } + public static function displayName(UserService $service): string + { + return 'camel:' . $service->name; + } - function UserService_other_name(UserService $service): string - { - return 'wrong method suffix'; - } + public static function profile_label(UserService $service): string + { + return 'snake:' . $service->name; + } - function UserService_CASECheck(UserService $service): string - { - return 'case-insensitive:' . $service->name; + public static function CASECheck(UserService $service): string + { + return 'case-insensitive:' . $service->name; + } } } diff --git a/tests/compiler/universal_method/universal_method_extension.phpt b/tests/compiler/universal_method/universal_method_extension.phpt index b50a8ed3..35a97241 100644 --- a/tests/compiler/universal_method/universal_method_extension.phpt +++ b/tests/compiler/universal_method/universal_method_extension.phpt @@ -1,23 +1,35 @@ --TEST-- -Universal method: extension functions +Universal methods provided by ExtensionProvider classes --FILE-- 'one', 2 => 'two', 3 => 'three']; - return $map[$int] ?? 'unknown'; + public static function to_words(int $int): string + { + $map = [1 => 'one', 2 => 'two', 3 => 'three']; + return $map[$int] ?? 'unknown'; + } } -function str_double(string $str): string +#[ExtensionProvider(complex_types::type_string)] +final class StringExtensions { - return $str . $str; -} + public static function double(string $str): string + { + return $str . $str; + } -function str_get_length(string $str): int -{ - return strlen($str); -} + public static function get_length(string $str): int + { + return strlen($str); + } -function str_to_array(string $str, string $delimiter): array -{ - return $str->split($delimiter); + public static function to_array(string $str, string $delimiter): array + { + return $str->split($delimiter); + } } -function array_last(array $arr): mixed { - if ($arr->count() === 0) { - return null; +#[ExtensionProvider(complex_types::type_array)] +final class ArrayExtensions +{ + public static function last(array $arr): mixed + { + if ($arr->count() === 0) { + return null; + } + return $arr[$arr->count() - 1]; } - return $arr[$arr->count() - 1]; } function main() diff --git a/tests/compiler/universal_method/universal_method_internal.phpt b/tests/compiler/universal_method/universal_method_internal.phpt index 5eca4edc..ab92b9e1 100644 --- a/tests/compiler/universal_method/universal_method_internal.phpt +++ b/tests/compiler/universal_method/universal_method_internal.phpt @@ -1,10 +1,19 @@ --TEST-- -Universal method: PHP internal function as extension method via reflection +Universal method provider may wrap a PHP internal function --FILE--