From 7d7fa75d6f99a747be4d677709ccb191e618892a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 10 Aug 2026 16:20:38 +0800 Subject: [PATCH] feat(compiler): add dynamic scope tracking for runtime method calls - Add requiresDynamicScope parameter to genRuntimeObjectMethodCall - Implement markRuntimeObjectMethodCall to track methods needing dynamic scope - Add markInternalFunctionCallbackCall to handle PHP internal functions with callbacks - Create callback argument mapping for functions like array_map, usort, etc. - Add isScopeIndependentCallableExpr to identify closures and callables - Implement internalMethodMayInvokeCallback for reflection method tracking - Update method call traits to use dynamic scope marking logic - Add test case for reflection invocation preserving method visibility scope - Modify Translator to generate scope switch code for dynamic calls --- src/CompilerBase.php | 118 +++++++++++++++++- src/Parser/FunctionCallTrait.php | 1 + src/Parser/MethodCallTrait.php | 21 +++- src/Parser/NullsafeAccessTrait.php | 1 + src/Translator.php | 4 +- .../reflection/method-visibility-scope.phpt | 65 ++++++++++ 6 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 tests/compiler/reflection/method-visibility-scope.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 18f3b791..8b148eee 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3084,11 +3084,127 @@ class CompilerBase implements PropertyAccessContext string $method, array $args, string $funcName = '', - string $className = '' + string $className = '', + bool $requiresDynamicScope = true, ): string { + if ($requiresDynamicScope) { + $this->markRuntimeObjectMethodCall(); + } return $object . '.call(' . $method . ', ' . $this->parseCallArgs($args, $funcName, $className) . ')'; } + /** Mark methods that need Zend user-frame scope for dynamic visibility checks. */ + protected function markRuntimeObjectMethodCall(): void + { + if ($this->methodDef) { + $this->methodDef->hasDynamicCall = true; + } + } + + /** + * Mark PHP internal functions that synchronously invoke a user callback. + * Closures and first-class callables already retain their creation scope. + * + * @param array $args + */ + protected function markInternalFunctionCallbackCall(string $function, array $args): void + { + if (!$this->methodDef || $args === []) { + return; + } + + /** + * Reflection metadata is deliberately not used here. This code is also + * compiled by TypePHP itself, so the result must not depend on how a + * particular PHP build exposes callable parameter types. + * + * Each entry identifies callback arguments by their positional index + * and PHP named-argument name. Negative indexes count from the end. + * The boolean flag marks containers whose values are callbacks. + */ + static $callbackArgs = [ + 'array_map' => [[0, 'callback']], + 'array_filter' => [[1, 'callback']], + 'array_reduce' => [[1, 'callback']], + 'array_all' => [[1, 'callback']], + 'array_any' => [[1, 'callback']], + 'array_find' => [[1, 'callback']], + 'array_find_key' => [[1, 'callback']], + 'array_walk' => [[1, 'callback']], + 'array_walk_recursive' => [[1, 'callback']], + 'usort' => [[1, 'callback']], + 'uasort' => [[1, 'callback']], + 'uksort' => [[1, 'callback']], + 'call_user_func' => [[0, 'callback']], + 'call_user_func_array' => [[0, 'callback']], + 'forward_static_call' => [[0, 'callback']], + 'forward_static_call_array' => [[0, 'callback']], + 'preg_replace_callback' => [[1, 'callback']], + 'preg_replace_callback_array' => [[0, 'pattern', true]], + 'iterator_apply' => [[1, 'callback']], + 'array_udiff' => [[-1, 'value_compare_func']], + 'array_udiff_assoc' => [[-1, 'value_compare_func']], + 'array_uintersect' => [[-1, 'value_compare_func']], + 'array_uintersect_assoc' => [[-1, 'value_compare_func']], + 'array_diff_uassoc' => [[-1, 'key_compare_func']], + 'array_diff_ukey' => [[-1, 'key_compare_func']], + 'array_intersect_uassoc' => [[-1, 'key_compare_func']], + 'array_intersect_ukey' => [[-1, 'key_compare_func']], + 'array_udiff_uassoc' => [ + [-2, 'value_compare_func'], + [-1, 'key_compare_func'], + ], + 'array_uintersect_uassoc' => [ + [-2, 'value_compare_func'], + [-1, 'key_compare_func'], + ], + ]; + + $descriptors = $callbackArgs[strtolower(ltrim($function, '\\'))] ?? null; + if ($descriptors === null) { + return; + } + + $argCount = count($args); + foreach ($args as $index => $arg) { + if ($arg->unpack) { + // An unpacked argument can occupy any remaining callback slot. + $this->markRuntimeObjectMethodCall(); + return; + } + + foreach ($descriptors as $descriptor) { + [$position, $name] = $descriptor; + $container = $descriptor[2] ?? false; + $callbackPosition = $position < 0 ? $argCount + $position : $position; + $matches = $arg->name === null + ? $index === $callbackPosition + : $arg->name->toString() === $name; + if ($matches && ($container || !$this->isScopeIndependentCallableExpr($arg->value))) { + $this->markRuntimeObjectMethodCall(); + return; + } + } + } + } + + private function isScopeIndependentCallableExpr(Expr $expr): bool + { + return $expr instanceof Expr\Closure + || $expr instanceof Expr\ArrowFunction + || ($expr instanceof CallLike && $expr->isFirstClassCallable()) + || $this->isNull($expr); + } + + protected function internalMethodMayInvokeCallback(string $class, string $method): bool + { + static $methods = [ + 'reflectionmethod::invoke' => true, + 'reflectionmethod::invokeargs' => true, + ]; + return isset($methods[strtolower(ltrim($class, '\\') . '::' . $method)]); + } + protected function validateInternalNamedCallArgs(\ReflectionFunctionAbstract $ref, array $callArgs): void { $hasNamedArg = false; diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index 11a92d2c..a3955ad3 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -85,6 +85,7 @@ trait FunctionCallTrait $globalName = ltrim($name, '\\'); if ($this->isInternalFunction($globalName)) { $this->assertWasiFunctionSupported($expr, $globalName); + $this->markInternalFunctionCallbackCall($globalName, $expr->args); } if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) { $this->fatalError($expr, 'Unsupported function: `' . $name . '`'); diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 6b8f5620..3bc1d4c3 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -478,22 +478,33 @@ trait MethodCallTrait $funcName = ''; } + $requiresDynamicScope = true; if ($class && $funcName && !$magicMethod && $this->isInternalClass($class)) { $methodPtr = $this->getMethodPtr($class, $funcName); + // Calling a resolved public internal method does not require + // callback visibility scope. A small set of invoker methods is + // intentionally exempt because it executes another PHP method. + $requiresDynamicScope = $this->internalMethodMayInvokeCallback($class, $funcName); } else { $methodPtr = $method; } - if ($object === 'this_' or $object === 'self' or $object === 'static') { - $this->methodDef->hasDynamicCall = true; - } - if (empty($expr->args)) { + if ($requiresDynamicScope) { + $this->markRuntimeObjectMethodCall(); + } return $object . '.call(' . $methodPtr . ')'; } try { $class = empty($class) ? self::DYNAMIC_CALLED_CLASS : $class; - return $this->genRuntimeObjectMethodCall($object, $methodPtr, $expr->args, $funcName, $class); + return $this->genRuntimeObjectMethodCall( + $object, + $methodPtr, + $expr->args, + $funcName, + $class, + $requiresDynamicScope, + ); } catch (PlaceHolder) { return $this->genPlaceHolder($this->genArray([$object, $method])); } diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index 2c600433..dc9b9293 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -87,6 +87,7 @@ trait NullsafeAccessTrait $update = $this->escapeAttrMode($this->isPropertyFetchUpdate($item[2])); $code .= $this->getIndent() . "{$tmpVar} = {$object}.attr({$item[1]}, {$update});"; } else { + $this->markRuntimeObjectMethodCall(); $beforeStmtCount = count($this->context->beforeStmtLines); $afterStmtCount = count($this->context->afterStmtLines); $args = $this->parseCallArgs($item[2]); diff --git a/src/Translator.php b/src/Translator.php index 29f3a5a3..bb0e5090 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3707,7 +3707,9 @@ CODE; } $code .= $this->genDebugInfo(null, $debugName, $v->getStartLine()); - // 函数中存在动态调用的函数,需要在运行时动态切换作用域 + // AOT methods containing a Zend dynamic object call must expose their + // class scope to callable visibility checks. Pure native methods avoid + // this frame traversal and scope mutation entirely. if ($this->methodDef and $this->methodDef->hasDynamicCall) { $code .= $this->genScopeSwitchCode(); } diff --git a/tests/compiler/reflection/method-visibility-scope.phpt b/tests/compiler/reflection/method-visibility-scope.phpt new file mode 100644 index 00000000..dbcedd4a --- /dev/null +++ b/tests/compiler/reflection/method-visibility-scope.phpt @@ -0,0 +1,65 @@ +--TEST-- +Reflection invocation preserves AOT method visibility scope +--FILE-- +hidden(); + } + + public static function mapProtected(): array + { + return array_map([self::class, 'decorate'], ['callback']); + } + + public static function invokeProtected(): string + { + $method = new ReflectionMethod(self::class, 'decorate'); + return $method->invoke(null, 'reflection'); + } + + public static function invokeProtectedArgs(): string + { + $method = new ReflectionMethod(self::class, 'decorate'); + return $method->invokeArgs(null, ['reflection-args']); + } +} + +function main(): void +{ + $factory = new ReflectionMethod(ReflectionVisibilityBox::class, 'create'); + var_dump($factory->invoke(null)); + + $hidden = new ReflectionMethod(ReflectionVisibilityBox::class, 'hidden'); + $hidden->setAccessible(true); + var_dump($hidden->invoke(new ReflectionVisibilityBox())); + + var_dump(ReflectionVisibilityBox::mapProtected()); + var_dump(ReflectionVisibilityBox::invokeProtected()); + var_dump(ReflectionVisibilityBox::invokeProtectedArgs()); +} + +?> +--EXPECT-- +string(12) "protected-ok" +string(12) "protected-ok" +array(1) { + [0]=> + string(10) "[callback]" +} +string(12) "[reflection]" +string(17) "[reflection-args]"