From 7623d367edc611712204859ed76da70c750e388f Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 14 Sep 2026 18:27:01 +0800 Subject: [PATCH] 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 --- src/Parser/MethodCallTrait.php | 23 +++++--- src/Preprocessor.php | 13 +++++ .../typed-property-nullable-reference.phpt | 39 ++++++++++++++ ...-property-protected-runtime-signature.phpt | 54 +++++++++++++++++++ 4 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 tests/compiler/devirtualize/typed-property-nullable-reference.phpt create mode 100644 tests/compiler/devirtualize/typed-property-protected-runtime-signature.phpt diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index ec3cbe7d..0366923b 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -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 { diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 545352c9..98e15fc4 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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. diff --git a/tests/compiler/devirtualize/typed-property-nullable-reference.phpt b/tests/compiler/devirtualize/typed-property-nullable-reference.phpt new file mode 100644 index 00000000..ee944086 --- /dev/null +++ b/tests/compiler/devirtualize/typed-property-nullable-reference.phpt @@ -0,0 +1,39 @@ +--TEST-- +Nullable typed property receiver preserves by-reference arguments when non-null +--FILE-- +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"] diff --git a/tests/compiler/devirtualize/typed-property-protected-runtime-signature.phpt b/tests/compiler/devirtualize/typed-property-protected-runtime-signature.phpt new file mode 100644 index 00000000..4675596d --- /dev/null +++ b/tests/compiler/devirtualize/typed-property-protected-runtime-signature.phpt @@ -0,0 +1,54 @@ +--TEST-- +Typed property reference arguments follow the runtime protected override or __call target +--FILE-- +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"]