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
pull/48/head
韩天峰 2 weeks ago
parent 64afbc9656
commit 7d7fa75d6f
  1. 118
      src/CompilerBase.php
  2. 1
      src/Parser/FunctionCallTrait.php
  3. 21
      src/Parser/MethodCallTrait.php
  4. 1
      src/Parser/NullsafeAccessTrait.php
  5. 4
      src/Translator.php
  6. 65
      tests/compiler/reflection/method-visibility-scope.phpt

@ -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<Node\Arg> $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;

@ -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 . '`');

@ -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]));
}

@ -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]);

@ -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();
}

@ -0,0 +1,65 @@
--TEST--
Reflection invocation preserves AOT method visibility scope
--FILE--
<?php
class ReflectionVisibilityBox
{
protected function hidden(): string
{
return 'protected-ok';
}
protected static function decorate(string $value): string
{
return "[{$value}]";
}
public static function create(): string
{
$box = new static();
return $box->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]"
Loading…
Cancel
Save