fix(parser): handle nullable typed properties with by-reference method calls

- Fixed private method accessibility check to properly return empty string
  when method is private and inaccessible
- Updated property signature resolution to preserve method metadata for
  nullable object properties
- Modified final class check to require non-nullability for typed property
  receiver optimization
- Added test case for nullable typed property preserving by-reference
  arguments when non-null
- Added test case for typed property following runtime protected override
 or __call target
master
韩天峰 4 weeks ago
parent 137a584dc5
commit 7623d367ed
  1. 23
      src/Parser/MethodCallTrait.php
  2. 13
      src/Preprocessor.php
  3. 39
      tests/compiler/devirtualize/typed-property-nullable-reference.phpt
  4. 54
      tests/compiler/devirtualize/typed-property-protected-runtime-signature.phpt

@ -189,7 +189,14 @@ trait MethodCallTrait
$classDef = $this->getClass($class);
while (true) {
if ($classDef->hasMethod($method) || $classDef->hasAbstractMethod($method)) {
return $this->checkAccessible($classDef, $classDef->getMethodFlags($method)) ? $class : '';
$flags = $classDef->getMethodFlags($method);
// Protected overrides must keep the parent's by-reference
// contract even when they widen visibility to public. Private
// methods are unrelated declarations and cannot describe a
// runtime child method or __call() arguments.
return ($flags & Modifiers::PRIVATE) && !$this->checkAccessible($classDef, $flags)
? ''
: $class;
}
if (!$classDef->extends || !$this->hasClass($classDef->extends)) {
break;
@ -619,10 +626,10 @@ trait MethodCallTrait
$materializedNativeReceiver = true;
} elseif (($expr->var instanceof Expr\PropertyFetch || $expr->var instanceof Expr\StaticPropertyFetch)
&& $this->isIdExpr($expr->var->name)) {
// A non-nullable declared object property supplies a method
// signature, but only a final class proves the concrete receiver
// needed for a direct native call. Materialize either kind of
// property once before arguments (including hook getters).
// A declared object property supplies a method signature, but only
// a non-nullable final class proves the concrete receiver needed
// for a direct native call. Materialize either kind of property
// once before arguments (including hook getters).
$resolvedStaticProperty = false;
if ($expr->var instanceof Expr\PropertyFetch) {
$this->getPropertyIdentifier($expr->var, $expr->var->var, $expr->var->name);
@ -632,8 +639,8 @@ trait MethodCallTrait
}
$property = $this->getNativePropertyDef($expr->var);
if ($property !== null
&& $property->type === Type::OBJECT
&& !$property->nullable
&& ($property->type === Type::OBJECT
|| ($property->nullable && $property->type === Type::VAR))
&& $property->class !== ''
&& ($this->hasClass($property->class) || $this->hasInterface($property->class))
&& !$this->isNativeObjectClass($property->class)
@ -642,7 +649,7 @@ trait MethodCallTrait
$object = $this->parseOrderedOperand($expr->var, false, true);
$class = $property->class;
$typedPropertyReceiver = true;
if ($this->isFinalClass($property->class)) {
if (!$property->nullable && $this->isFinalClass($property->class)) {
$typedPropertyFinalClass = $property->class;
}
} else {

@ -2163,6 +2163,19 @@ class Preprocessor extends CompilerBase
// validation (callable as an intersection/DNF member is rejected
// there, ahead of the property-specific rule, matching Zend).
[$type, $class] = $this->resolveTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY);
// Nullable Zend objects use php::Var storage, but their single class
// declaration still provides the method signature needed to prepare
// by-reference arguments. Keep that metadata without narrowing the
// storage type or assuming the runtime value is non-null.
if ($typeNode instanceof NullableType && $typeNode->type instanceof Node\Name) {
[$nullableType, $nullableClass] = $this->resolveTypeDecl(
$typeNode->type,
self::DECL_TYPE_OF_PROPERTY,
);
if ($nullableType === Type::OBJECT) {
$class = $nullableClass;
}
}
// `callable` is a runtime-context type (a string or array may or may
// not be callable depending on scope), so Zend forbids it in property
// types entirely - bare, nullable, or as a union member.

@ -0,0 +1,39 @@
--TEST--
Nullable typed property receiver preserves by-reference arguments when non-null
--FILE--
<?php
final class TypedPropertyNullableTarget
{
public function record(array &$events): void
{
$events[] = 'recorded';
}
}
final class TypedPropertyNullableHolder
{
public ?TypedPropertyNullableTarget $target;
public function __construct()
{
$this->target = new TypedPropertyNullableTarget();
}
public function run(array &$events): void
{
$this->target->record($events);
}
}
function main(): void
{
$events = [];
$holder = new TypedPropertyNullableHolder();
$holder->run($events);
echo json_encode($events, JSON_THROW_ON_ERROR), "\n";
}
?>
--EXPECT--
["recorded"]

@ -0,0 +1,54 @@
--TEST--
Typed property reference arguments follow the runtime protected override or __call target
--FILE--
<?php
class TypedPropertyProtectedBase
{
protected function record(array &$events): void
{
}
public function __call(string $name, array $arguments): void
{
$arguments[0][] = 'magic';
}
}
final class TypedPropertyProtectedChild extends TypedPropertyProtectedBase
{
public function record(array &$events): void
{
$events[] = 'child';
}
}
final class TypedPropertyProtectedHolder
{
public TypedPropertyProtectedBase $target;
public function run(array &$events): void
{
$this->target->record($events);
}
}
function main(): void
{
$holder = new TypedPropertyProtectedHolder();
$magicEvents = [];
$holder->target = new TypedPropertyProtectedBase();
$holder->run($magicEvents);
echo json_encode($magicEvents, JSON_THROW_ON_ERROR), "\n";
$childEvents = [];
$holder->target = new TypedPropertyProtectedChild();
$holder->run($childEvents);
echo json_encode($childEvents, JSON_THROW_ON_ERROR), "\n";
}
?>
--EXPECT--
[]
["child"]
Loading…
Cancel
Save