diff --git a/phpunit/code/trait-method-override-final.php b/phpunit/code/trait-method-override-final.php new file mode 100644 index 00000000..2cf6beeb --- /dev/null +++ b/phpunit/code/trait-method-override-final.php @@ -0,0 +1,24 @@ +execute('ok')); +} diff --git a/phpunit/code/trait-parent-without-parent.php b/phpunit/code/trait-parent-without-parent.php new file mode 100644 index 00000000..c3a025d2 --- /dev/null +++ b/phpunit/code/trait-parent-without-parent.php @@ -0,0 +1,18 @@ +exec('Cannot access private method `BaseSecret::secret()`', 'trait-parent-method-private.php'); } + public function testTraitMethodMayShadowPrivateParentMethod() + { + $this->compile('trait-method-shadows-private.php'); + } + public function testSelfCanBePartOfUnionType() { global $translator; diff --git a/phpunit/src/InheritanceErrorTest.php b/phpunit/src/InheritanceErrorTest.php index d7ad5cbf..59c07e14 100644 --- a/phpunit/src/InheritanceErrorTest.php +++ b/phpunit/src/InheritanceErrorTest.php @@ -325,4 +325,14 @@ class InheritanceErrorTest extends TestCase // runtime fatal error that the compiled binary would otherwise ignore. $this->exec('must be compatible', 'trait-method-override-incompatible.php'); } + + public function testTraitMethodCannotOverrideFinalParentMethod() + { + $this->exec('Cannot override final method', 'trait-method-override-final.php'); + } + + public function testTraitParentCallRequiresParentClass() + { + $this->exec('has no parent', 'trait-parent-without-parent.php'); + } } diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 79431f24..826516eb 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -348,7 +348,8 @@ trait CallArgumentGenerator string $funcName = '', string $className = '', bool $separateNamedArgs = true, - bool $forceArrayArgs = false + bool $forceArrayArgs = false, + bool $preserveExistingReferences = false ): string { $list_args = []; @@ -390,7 +391,8 @@ trait CallArgumentGenerator $this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`"); } $namedArgs[$arg->name->name] = true; - $byRef = $funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name); + $byRef = ($funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name)) + || ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg)); $value = ($byRef || $this->isRefvalCall($arg->value) || $this->isToRefCall($arg->value)) ? $this->parseReferenceCallArgValue($arg) : $this->parseCallArgValue($arg); @@ -409,7 +411,8 @@ trait CallArgumentGenerator if ($hasUnpack) { $this->fatalError($arg, 'Cannot use positional argument after argument unpacking'); } - $byRef = $funcName && $this->isReferenceArgument($funcName, $className, $i); + $byRef = ($funcName && $this->isReferenceArgument($funcName, $className, $i)) + || ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg)); if (($funcName === 'call_user_func' || $funcName === 'call_user_func_array') && $i === 0) { $callback = $this->parseScopedCallbackArg($arg); if ($callback !== null) { @@ -499,6 +502,15 @@ trait CallArgumentGenerator return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs; } + private function isExistingReferenceCallArg(Node\Arg $arg): bool + { + if (!$this->isVarExpr($arg->value)) { + return $this->isReferenceWrapperCall($arg->value); + } + $name = $this->parseIdentifier($arg->value); + return $this->hasVar($name) && $this->getVarType($name) === Type::REF; + } + protected function parseScopedCallbackArg(Node\Arg $arg): ?string { $value = $arg->value; @@ -716,6 +728,8 @@ trait CallArgumentGenerator if (!$this->hasVar($name)) { // 若参数是引用类型,可以传入未定义变量,将立即创建变量作为引用 $this->addLocalVar($name, Type::REF); + } elseif ($this->getVarType($name) === Type::REF) { + return '&' . $name; } else { // 本地变量,且是原生类型,则转为普通变量 if ($this->hasLocalVar($name) and $this->isNativeType($this->getVarType($name))) { @@ -788,4 +802,3 @@ trait CallArgumentGenerator } } - diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 57e21d57..6ede7f2c 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -243,11 +243,10 @@ trait MethodCallTrait protected function parseParentMethodCall(Expr\StaticCall $expr): string { - // Inside a trait, `parent::` refers to the parent of the class that - // *uses* the trait. That parent class is only known at runtime (a single - // trait may be composed into classes with different parents), so resolve - // it dynamically from the current object's class entry instead of the - // trait's own (non-existent) parent. + // A trait's parent scope is supplied by the wrapper generated for the + // class that composes it. It must not be derived from the runtime + // object's class: an inherited trait method is still lexically bound to + // the parent of the composing class, not to the runtime object's parent. if ($this->classDef !== null && $this->classDef->trait !== null) { $method = $this->isIdExpr($expr->name) ? $this->parseIdentifier($expr->name) : ''; // Record the parent:: call so it can be validated against the parent @@ -256,12 +255,19 @@ trait MethodCallTrait if ($method !== '' && isset($this->methodDef)) { $this->methodDef->parentMethodCalls[] = ['method' => $method, 'node' => $expr]; } - $methodPtr = 'php::getMethod(this_.parent_ce(), ' . $this->identifierToStr($expr->name) . ')'; + $methodPtr = 'php::getMethod(trait_parent_ce, ' . $this->identifierToStr($expr->name) . ')'; if (empty($expr->args)) { return 'this_.call(' . $methodPtr . ')'; } - // Parent class is unknown statically, so by-ref argument detection is skipped. - return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args, $method, '') . ')'; + // The concrete parent signature is only known at each trait use + // site. Preserve arguments that are already references so forwarding + // a by-reference trait parameter does not silently drop its alias. + return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs( + $expr->args, + $method, + '', + preserveExistingReferences: true + ) . ')'; } if (!$this->classDef->extends) { diff --git a/src/Translator.php b/src/Translator.php index 8ddf5cde..1f15eca3 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2506,8 +2506,7 @@ CODE; if ($fn->returnTypeKeyword !== '' && $methodStmt->returnType instanceof Node\Name) { if ($fn->returnTypeKeyword === 'static') { - // `static` is late-static-bound: keep the keyword so ZendVM - // resolves it to the concrete class at call time. + // `static` remains late-bound in the composed method signature. $methodStmt->returnType = new Node\Name('static'); } else { $resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); @@ -2734,7 +2733,12 @@ CODE; return $code; } - protected function genWrapperFunctionArgs(string $fn, FunctionDef $functionDef, string $displayName): string + protected function genWrapperFunctionArgs( + string $fn, + FunctionDef $functionDef, + string $displayName, + array $implicitMethodArgs = [] + ): string { $cppCode = ''; $callParams = ''; @@ -2783,7 +2787,8 @@ CODE; } if ($functionDef->method) { - $callParams = $functionDef->argInfoList ? 'this_, ' . rtrim($callParams, ',') : 'this_'; + $methodArgs = implode(', ', array_merge(['this_'], $implicitMethodArgs)); + $callParams = $functionDef->argInfoList ? $methodArgs . ', ' . rtrim($callParams, ',') : $methodArgs; } else { $callParams = $functionDef->argInfoList ? rtrim($callParams, ',') : ''; } @@ -2844,7 +2849,18 @@ CODE; $cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL; $cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL; $fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef); - $cppCode .= $this->genWrapperFunctionArgs($fn, $methodDef->functionDef, $classDef->getNamespacedName(false) . '::' . $methodDef->name); + $implicitMethodArgs = []; + if ($classDef->trait !== null && $methodDef->parentMethodCalls) { + // Trait methods are not directly callable without a composing class, + // but keep the generated Zend wrapper well-formed. + $implicitMethodArgs[] = 'this_.parent_ce()'; + } + $cppCode .= $this->genWrapperFunctionArgs( + $fn, + $methodDef->functionDef, + $classDef->getNamespacedName(false) . '::' . $methodDef->name, + $implicitMethodArgs + ); return $cppCode; } @@ -3041,6 +3057,9 @@ CODE; $functionDeclCode = $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; if ($this->class) { $functionDeclCode .= Type::OBJECT . ' &this_'; + if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) { + $functionDeclCode .= ', zend_class_entry *trait_parent_ce'; + } if ($this->functionDef->params) { $functionDeclCode .= ', '; } @@ -3733,6 +3752,11 @@ CODE; $traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name); $classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name); $argList = ['this_']; + if ($methodDef->parentMethodCalls) { + // Bind parent:: to the class that actually composes the trait. This + // remains correct when the generated wrapper is inherited further. + $argList[] = $this->getClassEntryPtr($classDef->extends); + } foreach ($methodDef->functionDef->argInfoList as $argInfo) { $argList[] = $argInfo->name; } @@ -3853,8 +3877,10 @@ CODE; private function validateTraitParentCall(ClassDef $traitDef, ClassDef $usingClassDef, string $method, NodeAbstract $node): void { if (!$usingClassDef->extends) { - // No parent class: cannot validate; PHP would report at runtime. - return; + $this->fatalError( + $node, + "Cannot access parent when class `{$usingClassDef->getNamespacedName(false)}` has no parent" + ); } $parentClass = $usingClassDef->extends; // Internal / not-compiled parents are opaque to the compiler; let the @@ -3895,18 +3921,33 @@ CODE; if (!$extends) { break; } - // Internal / not-compiled parents are opaque to the compiler; let the - // runtime enforce compatibility for those. - if ($classDef->inheritedFromInternalClass || !$this->hasClass($extends)) { + if ($classDef->inheritedFromInternalClass) { + $modifiers = Reflection::getClassMethodModifiers($extends, $methodName); + if ($modifiers !== null && ($modifiers & \ReflectionMethod::IS_FINAL)) { + $this->fatalError($methodDef->node, "Cannot override final method `{$extends}::{$methodName}()`"); + } + break; + } + // Dynamically supplied parents are opaque to the compiler. + if (!$this->hasClass($extends)) { break; } $classDef = $this->getClass($extends); if ($classDef->hasMethod($methodName)) { + $parentMethodDef = $classDef->getMethod($methodName); + // A private method is a separate slot and may be shadowed by the + // method imported from the trait. + if ($parentMethodDef->flags & Modifiers::PRIVATE) { + break; + } + if ($parentMethodDef->flags & Modifiers::FINAL) { + $this->fatalError($methodDef->node, "Cannot override final method `{$extends}::{$methodName}()`"); + } $this->validateMethodOverrideSignature( $methodDef->node, $methodName, $methodDef, - $classDef->getMethod($methodName), + $parentMethodDef, $extends ); break; diff --git a/tests/compiler/trait/trait-parent-method-inherited.phpt b/tests/compiler/trait/trait-parent-method-inherited.phpt new file mode 100644 index 00000000..c6f5a752 --- /dev/null +++ b/tests/compiler/trait/trait-parent-method-inherited.phpt @@ -0,0 +1,52 @@ +--TEST-- +Trait parent:: call remains bound to the composing class when inherited +--FILE-- +source()); + var_dump((new OtherTraitUser())->source()); +} +?> +--EXPECT-- +string(4) "root" +string(5) "other" diff --git a/tests/compiler/trait/trait-parent-method-reference.phpt b/tests/compiler/trait/trait-parent-method-reference.phpt new file mode 100644 index 00000000..4e72026d --- /dev/null +++ b/tests/compiler/trait/trait-parent-method-reference.phpt @@ -0,0 +1,36 @@ +--TEST-- +Trait parent:: call forwards an existing reference parameter +--FILE-- +update($value); + var_dump($value); +} +?> +--EXPECT-- +string(7) "updated"