feat(parser): add runtime method scope detection for dynamic calls

- Introduced runtimeMethodRequiresDynamicScope to check method visibility
- Integrated reflection-based method modifier checking
- Updated nullsafe access trait to pass expression objects for scope analysis
- Added test case for nullsafe method calls preserving private visibility
- Refactored method call processing to handle dynamic scope requirements
pull/48/head
韩天峰 2 weeks ago
parent 7190bea4a5
commit b77fee697e
  1. 56
      src/Parser/MethodCallTrait.php
  2. 12
      src/Parser/NullsafeAccessTrait.php
  3. 29
      tests/compiler/nullsafe/nullsafe-private-scope.phpt

@ -16,9 +16,46 @@ use PhpParser\Node\Expr\CallLike;
use TypePhp\Exception\DynamicCall;
use TypePhp\Exception\PlaceHolder;
use TypePhp\Generator\Symbol;
use TypePhp\Resolver\Reflection;
trait MethodCallTrait
{
protected function runtimeMethodRequiresDynamicScope(
string $class,
string $method,
bool $magicMethod = false,
): bool {
if ($method === '' || $magicMethod) {
return true;
}
if ($class !== '') {
$flags = $this->getMethodFlags($class, $method);
if ($flags !== 0) {
return !($flags & Modifiers::PUBLIC);
}
$modifiers = Reflection::getClassMethodModifiers($class, $method);
if ($modifiers !== null) {
return !($modifiers & \ReflectionMethod::IS_PUBLIC);
}
}
// Late-bound receivers such as `new static()` do not have an exact
// class in the local type map. A matching current-class method still
// carries the lexical visibility rules of that class.
if ($this->classDef !== null) {
$flags = $this->getMethodFlags($this->getFullClassName(), $method);
if ($flags !== 0) {
return !($flags & Modifiers::PUBLIC);
}
}
// A named method with no non-public declaration is resolved as a
// normal public call and must not change an unrelated Zend frame.
return false;
}
protected function isOverrideMethod(string $fullMethodName): bool
{
$fullMethodNameLower = strtolower($fullMethodName);
@ -488,13 +525,18 @@ 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);
$requiresDynamicScope = $this->runtimeMethodRequiresDynamicScope($class, $funcName, $magicMethod);
if ($class && $funcName && !$magicMethod) {
if ($this->isInternalClass($class)) {
$methodPtr = $this->getMethodPtr($class, $funcName);
// A small set of internal invokers synchronously executes a
// callback and therefore still needs the caller's scope.
if ($this->internalMethodMayInvokeCallback($class, $funcName)) {
$requiresDynamicScope = true;
}
} else {
$methodPtr = $method;
}
} else {
$methodPtr = $method;
}

@ -46,13 +46,13 @@ trait NullsafeAccessTrait
$list[] = ['property', $this->identifierToStr($expr->name, literal: true), $expr, true];
$expr = $expr->var;
} elseif ($expr instanceof Expr\NullsafeMethodCall) {
$list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, true];
$list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, true, $expr];
$expr = $expr->var;
} elseif ($expr instanceof Expr\PropertyFetch) {
$list[] = ['property', $this->identifierToStr($expr->name, literal: true), $expr, false];
$expr = $expr->var;
} elseif ($expr instanceof Expr\MethodCall) {
$list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, false];
$list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args, false, $expr];
$expr = $expr->var;
} else {
if ($this->isVarExpr($expr)) {
@ -87,7 +87,13 @@ trait NullsafeAccessTrait
$update = $this->escapeAttrMode($this->isPropertyFetchUpdate($item[2]));
$code .= $this->getIndent() . "{$tmpVar} = {$object}.attr({$item[1]}, {$update});";
} else {
$this->markRuntimeObjectMethodCall();
$methodName = $this->isNamedMethod($item[4]->name)
? $this->parseIdentifier($item[4]->name)
: '';
$receiverClass = $this->detectClassOfExpr($item[4]->var);
if ($this->runtimeMethodRequiresDynamicScope($receiverClass, $methodName)) {
$this->markRuntimeObjectMethodCall();
}
$beforeStmtCount = count($this->context->beforeStmtLines);
$afterStmtCount = count($this->context->afterStmtLines);
$args = $this->parseCallArgs($item[2]);

@ -0,0 +1,29 @@
--TEST--
Nullsafe method calls preserve private visibility when required
--FILE--
<?php
final class NullsafePrivateScope
{
private function value(): string
{
return 'private-ok';
}
public function read(?self $object): ?string
{
return $object?->value();
}
}
function main(): void
{
$object = new NullsafePrivateScope();
var_dump($object->read($object));
var_dump($object->read(null));
}
?>
--EXPECT--
string(10) "private-ok"
NULL
Loading…
Cancel
Save