fix(compiler): preserve trait parent scope

pull/26/head
韩天峰 1 month ago
parent 33d35435ba
commit f34fe1844d
  1. 24
      phpunit/code/trait-method-override-final.php
  2. 27
      phpunit/code/trait-method-shadows-private.php
  3. 18
      phpunit/code/trait-parent-without-parent.php
  4. 5
      phpunit/src/ClassTest.php
  5. 10
      phpunit/src/InheritanceErrorTest.php
  6. 21
      src/Generator/CallArgumentGenerator.php
  7. 22
      src/Parser/MethodCallTrait.php
  8. 63
      src/Translator.php
  9. 52
      tests/compiler/trait/trait-parent-method-inherited.phpt
  10. 36
      tests/compiler/trait/trait-parent-method-reference.phpt

@ -0,0 +1,24 @@
<?php
trait FinalOverrideTrait
{
public function execute(): void
{
}
}
class FinalMethodParent
{
final public function execute(): void
{
}
}
class FinalMethodChild extends FinalMethodParent
{
use FinalOverrideTrait;
}
function main(): void
{
}

@ -0,0 +1,27 @@
<?php
trait PrivateShadowTrait
{
public function execute(string $value): string
{
return $value;
}
}
class PrivateMethodParent
{
private function execute(int $value, bool $flag): int
{
return $value;
}
}
class PrivateMethodChild extends PrivateMethodParent
{
use PrivateShadowTrait;
}
function main(): void
{
var_dump((new PrivateMethodChild())->execute('ok'));
}

@ -0,0 +1,18 @@
<?php
trait MissingParentTrait
{
public function execute(): void
{
parent::execute();
}
}
class NoParentClass
{
use MissingParentTrait;
}
function main(): void
{
}

@ -54,6 +54,11 @@ class ClassTest extends \BaseTest
$this->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;

@ -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');
}
}

@ -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
}
}

@ -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) {

@ -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;

@ -0,0 +1,52 @@
--TEST--
Trait parent:: call remains bound to the composing class when inherited
--FILE--
<?php
trait ParentCallTrait
{
public function source(): string
{
return parent::source();
}
}
class RootClass
{
public function source(): string
{
return 'root';
}
}
class TraitUser extends RootClass
{
use ParentCallTrait;
}
class ChildClass extends TraitUser
{
}
class OtherRootClass
{
public function source(): string
{
return 'other';
}
}
class OtherTraitUser extends OtherRootClass
{
use ParentCallTrait;
}
function main(): void
{
var_dump((new ChildClass())->source());
var_dump((new OtherTraitUser())->source());
}
?>
--EXPECT--
string(4) "root"
string(5) "other"

@ -0,0 +1,36 @@
--TEST--
Trait parent:: call forwards an existing reference parameter
--FILE--
<?php
trait ParentReferenceTrait
{
public function update(string &$value): void
{
parent::update($value);
}
}
class ReferenceParent
{
public function update(string &$value): void
{
$value = 'updated';
}
}
class ReferenceChild extends ReferenceParent
{
use ParentReferenceTrait;
}
function main(): void
{
$value = 'initial';
$child = new ReferenceChild();
$child->update($value);
var_dump($value);
}
?>
--EXPECT--
string(7) "updated"
Loading…
Cancel
Save