diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 31299ea3..8c4c74a8 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -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; } diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index dc9b9293..a1587f1d 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -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]); diff --git a/tests/compiler/nullsafe/nullsafe-private-scope.phpt b/tests/compiler/nullsafe/nullsafe-private-scope.phpt new file mode 100644 index 00000000..42964bec --- /dev/null +++ b/tests/compiler/nullsafe/nullsafe-private-scope.phpt @@ -0,0 +1,29 @@ +--TEST-- +Nullsafe method calls preserve private visibility when required +--FILE-- +value(); + } +} + +function main(): void +{ + $object = new NullsafePrivateScope(); + var_dump($object->read($object)); + var_dump($object->read(null)); +} + +?> +--EXPECT-- +string(10) "private-ok" +NULL