fix(compiler): preserve trait parent scope

pull/26/head
韩天峰 2 months 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'); $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() public function testSelfCanBePartOfUnionType()
{ {
global $translator; global $translator;

@ -325,4 +325,14 @@ class InheritanceErrorTest extends TestCase
// runtime fatal error that the compiled binary would otherwise ignore. // runtime fatal error that the compiled binary would otherwise ignore.
$this->exec('must be compatible', 'trait-method-override-incompatible.php'); $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 $funcName = '',
string $className = '', string $className = '',
bool $separateNamedArgs = true, bool $separateNamedArgs = true,
bool $forceArrayArgs = false bool $forceArrayArgs = false,
bool $preserveExistingReferences = false
): string ): string
{ {
$list_args = []; $list_args = [];
@ -390,7 +391,8 @@ trait CallArgumentGenerator
$this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`"); $this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`");
} }
$namedArgs[$arg->name->name] = true; $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)) $value = ($byRef || $this->isRefvalCall($arg->value) || $this->isToRefCall($arg->value))
? $this->parseReferenceCallArgValue($arg) ? $this->parseReferenceCallArgValue($arg)
: $this->parseCallArgValue($arg); : $this->parseCallArgValue($arg);
@ -409,7 +411,8 @@ trait CallArgumentGenerator
if ($hasUnpack) { if ($hasUnpack) {
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking'); $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) { if (($funcName === 'call_user_func' || $funcName === 'call_user_func_array') && $i === 0) {
$callback = $this->parseScopedCallbackArg($arg); $callback = $this->parseScopedCallbackArg($arg);
if ($callback !== null) { if ($callback !== null) {
@ -499,6 +502,15 @@ trait CallArgumentGenerator
return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs; 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 protected function parseScopedCallbackArg(Node\Arg $arg): ?string
{ {
$value = $arg->value; $value = $arg->value;
@ -716,6 +728,8 @@ trait CallArgumentGenerator
if (!$this->hasVar($name)) { if (!$this->hasVar($name)) {
// 若参数是引用类型,可以传入未定义变量,将立即创建变量作为引用 // 若参数是引用类型,可以传入未定义变量,将立即创建变量作为引用
$this->addLocalVar($name, Type::REF); $this->addLocalVar($name, Type::REF);
} elseif ($this->getVarType($name) === Type::REF) {
return '&' . $name;
} else { } else {
// 本地变量,且是原生类型,则转为普通变量 // 本地变量,且是原生类型,则转为普通变量
if ($this->hasLocalVar($name) and $this->isNativeType($this->getVarType($name))) { 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 protected function parseParentMethodCall(Expr\StaticCall $expr): string
{ {
// Inside a trait, `parent::` refers to the parent of the class that // A trait's parent scope is supplied by the wrapper generated for the
// *uses* the trait. That parent class is only known at runtime (a single // class that composes it. It must not be derived from the runtime
// trait may be composed into classes with different parents), so resolve // object's class: an inherited trait method is still lexically bound to
// it dynamically from the current object's class entry instead of the // the parent of the composing class, not to the runtime object's parent.
// trait's own (non-existent) parent.
if ($this->classDef !== null && $this->classDef->trait !== null) { if ($this->classDef !== null && $this->classDef->trait !== null) {
$method = $this->isIdExpr($expr->name) ? $this->parseIdentifier($expr->name) : ''; $method = $this->isIdExpr($expr->name) ? $this->parseIdentifier($expr->name) : '';
// Record the parent:: call so it can be validated against the parent // Record the parent:: call so it can be validated against the parent
@ -256,12 +255,19 @@ trait MethodCallTrait
if ($method !== '' && isset($this->methodDef)) { if ($method !== '' && isset($this->methodDef)) {
$this->methodDef->parentMethodCalls[] = ['method' => $method, 'node' => $expr]; $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)) { if (empty($expr->args)) {
return 'this_.call(' . $methodPtr . ')'; return 'this_.call(' . $methodPtr . ')';
} }
// Parent class is unknown statically, so by-ref argument detection is skipped. // The concrete parent signature is only known at each trait use
return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args, $method, '') . ')'; // 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) { if (!$this->classDef->extends) {

@ -2506,8 +2506,7 @@ CODE;
if ($fn->returnTypeKeyword !== '' && $methodStmt->returnType instanceof Node\Name) { if ($fn->returnTypeKeyword !== '' && $methodStmt->returnType instanceof Node\Name) {
if ($fn->returnTypeKeyword === 'static') { if ($fn->returnTypeKeyword === 'static') {
// `static` is late-static-bound: keep the keyword so ZendVM // `static` remains late-bound in the composed method signature.
// resolves it to the concrete class at call time.
$methodStmt->returnType = new Node\Name('static'); $methodStmt->returnType = new Node\Name('static');
} else { } else {
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); $resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword);
@ -2734,7 +2733,12 @@ CODE;
return $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 = ''; $cppCode = '';
$callParams = ''; $callParams = '';
@ -2783,7 +2787,8 @@ CODE;
} }
if ($functionDef->method) { if ($functionDef->method) {
$callParams = $functionDef->argInfoList ? 'this_, ' . rtrim($callParams, ',') : 'this_'; $methodArgs = implode(', ', array_merge(['this_'], $implicitMethodArgs));
$callParams = $functionDef->argInfoList ? $methodArgs . ', ' . rtrim($callParams, ',') : $methodArgs;
} else { } else {
$callParams = $functionDef->argInfoList ? rtrim($callParams, ',') : ''; $callParams = $functionDef->argInfoList ? rtrim($callParams, ',') : '';
} }
@ -2844,7 +2849,18 @@ CODE;
$cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL; $cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL;
$cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL; $cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL;
$fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef); $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; return $cppCode;
} }
@ -3041,6 +3057,9 @@ CODE;
$functionDeclCode = $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; $functionDeclCode = $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '(';
if ($this->class) { if ($this->class) {
$functionDeclCode .= Type::OBJECT . ' &this_'; $functionDeclCode .= Type::OBJECT . ' &this_';
if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) {
$functionDeclCode .= ', zend_class_entry *trait_parent_ce';
}
if ($this->functionDef->params) { if ($this->functionDef->params) {
$functionDeclCode .= ', '; $functionDeclCode .= ', ';
} }
@ -3733,6 +3752,11 @@ CODE;
$traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name); $traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name);
$classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name); $classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name);
$argList = ['this_']; $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) { foreach ($methodDef->functionDef->argInfoList as $argInfo) {
$argList[] = $argInfo->name; $argList[] = $argInfo->name;
} }
@ -3853,8 +3877,10 @@ CODE;
private function validateTraitParentCall(ClassDef $traitDef, ClassDef $usingClassDef, string $method, NodeAbstract $node): void private function validateTraitParentCall(ClassDef $traitDef, ClassDef $usingClassDef, string $method, NodeAbstract $node): void
{ {
if (!$usingClassDef->extends) { if (!$usingClassDef->extends) {
// No parent class: cannot validate; PHP would report at runtime. $this->fatalError(
return; $node,
"Cannot access parent when class `{$usingClassDef->getNamespacedName(false)}` has no parent"
);
} }
$parentClass = $usingClassDef->extends; $parentClass = $usingClassDef->extends;
// Internal / not-compiled parents are opaque to the compiler; let the // Internal / not-compiled parents are opaque to the compiler; let the
@ -3895,18 +3921,33 @@ CODE;
if (!$extends) { if (!$extends) {
break; break;
} }
// Internal / not-compiled parents are opaque to the compiler; let the if ($classDef->inheritedFromInternalClass) {
// runtime enforce compatibility for those. $modifiers = Reflection::getClassMethodModifiers($extends, $methodName);
if ($classDef->inheritedFromInternalClass || !$this->hasClass($extends)) { 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; break;
} }
$classDef = $this->getClass($extends); $classDef = $this->getClass($extends);
if ($classDef->hasMethod($methodName)) { 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( $this->validateMethodOverrideSignature(
$methodDef->node, $methodDef->node,
$methodName, $methodName,
$methodDef, $methodDef,
$classDef->getMethod($methodName), $parentMethodDef,
$extends $extends
); );
break; 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