fix: 修复trait中parent::调用及方法覆盖兼容性校验

pull/26/head
Yurun 1 month ago
parent 559a8860a0
commit 9d22e8ca24
  1. 23
      phpunit/code/parent-method-private.php
  2. 24
      phpunit/code/trait-method-override-incompatible.php
  3. 28
      phpunit/code/trait-parent-method-private.php
  4. 28
      phpunit/code/trait-parent-method-protected.php
  5. 17
      phpunit/src/ClassTest.php
  6. 9
      phpunit/src/InheritanceErrorTest.php
  7. 16
      src/Entity/MethodDef.php
  8. 26
      src/Parser/MethodCallTrait.php
  9. 102
      src/Translator.php
  10. 36
      tests/compiler/trait/trait-parent-constructor.phpt
  11. 34
      tests/compiler/trait/trait-parent-method-protected.phpt
  12. 34
      tests/compiler/trait/trait-parent-method.phpt

@ -0,0 +1,23 @@
<?php
class Base
{
private function secret(): string
{
return 'secret';
}
}
class Child extends Base
{
public function reveal(): string
{
return parent::secret();
}
}
function main(): void
{
$c = new Child();
var_dump($c->reveal());
}

@ -0,0 +1,24 @@
<?php
trait IncompatibleTrait
{
public function test(int $value)
{
}
}
class IncompatibleParent
{
protected function test(int $value, bool $bool)
{
}
}
class IncompatibleChild extends IncompatibleParent
{
use IncompatibleTrait;
}
function main(): void
{
}

@ -0,0 +1,28 @@
<?php
trait SecretTrait
{
public function reveal(): string
{
return parent::secret();
}
}
class BaseSecret
{
private function secret(): string
{
return 'secret';
}
}
class ChildSecret extends BaseSecret
{
use SecretTrait;
}
function main(): void
{
$c = new ChildSecret();
var_dump($c->reveal());
}

@ -0,0 +1,28 @@
<?php
trait GreetTrait
{
public function greet(string $name): string
{
return parent::greet($name) . ' [via trait]';
}
}
class BaseGreeter
{
protected function greet(string $name): string
{
return 'Hello ' . $name;
}
}
class ChildGreeter extends BaseGreeter
{
use GreetTrait;
}
function main(): void
{
$g = new ChildGreeter();
var_dump($g->greet('World'));
}

@ -37,6 +37,23 @@ class ClassTest extends \BaseTest
$this->exec('Cannot override private method `Base::doWork()`', 'override-private-method.php'); $this->exec('Cannot override private method `Base::doWork()`', 'override-private-method.php');
} }
public function testTraitMayCallProtectedParentMethod()
{
// A protected parent method is reachable via parent:: from a trait,
// matching PHP runtime behaviour.
$this->compile('trait-parent-method-protected.php');
}
public function testCannotAccessPrivateParentMethodFromRegularClass()
{
$this->exec('Cannot access private method `Base::secret()`', 'parent-method-private.php');
}
public function testCannotAccessPrivateParentMethodFromTrait()
{
$this->exec('Cannot access private method `BaseSecret::secret()`', 'trait-parent-method-private.php');
}
public function testSelfCanBePartOfUnionType() public function testSelfCanBePartOfUnionType()
{ {
global $translator; global $translator;

@ -306,4 +306,13 @@ class InheritanceErrorTest extends TestCase
{ {
$this->exec('must be compatible', 'abstract_method_signature_mismatch.php'); $this->exec('must be compatible', 'abstract_method_signature_mismatch.php');
} }
public function testTraitMethodMustBeCompatibleWithParent()
{
// A trait method flattened into a class must remain signature-compatible
// with any same-named parent method, just like a directly-declared
// override. Without this check the incompatibility only surfaces as a
// runtime fatal error that the compiled binary would otherwise ignore.
$this->exec('must be compatible', 'trait-method-override-incompatible.php');
}
} }

@ -15,6 +15,22 @@ class MethodDef
public ?FunctionDef $functionDef = null; public ?FunctionDef $functionDef = null;
public bool $hasDynamicCall = false; public bool $hasDynamicCall = false;
/**
* The original `ClassMethod` AST node this definition was parsed from.
* Stored so that later validation (e.g. trait method override compatibility
* checks performed at the `use` site) can report accurate line information.
*/
public ?\PhpParser\Node\Stmt\ClassMethod $node = null;
/**
* For methods defined inside a trait, records `parent::method()` calls so
* the compiler can validate their visibility against the parent of each
* class that uses the trait (the trait itself has no parent at compile time).
*
* @var array<int, array{method: string, node: \PhpParser\NodeAbstract}>
*/
public array $parentMethodCalls = [];
public function __construct(int $flags, string $name) public function __construct(int $flags, string $name)
{ {
$this->flags = $flags; $this->flags = $flags;

@ -243,6 +243,27 @@ 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
// *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.
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
// of every class that uses this trait (the trait itself has no parent
// at compile time). Dynamic method names cannot be validated statically.
if ($method !== '' && isset($this->methodDef)) {
$this->methodDef->parentMethodCalls[] = ['method' => $method, 'node' => $expr];
}
$methodPtr = 'php::getMethod(this_.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, '') . ')';
}
if (!$this->classDef->extends) { if (!$this->classDef->extends) {
$this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class'); $this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class');
} }
@ -250,6 +271,11 @@ trait MethodCallTrait
if ($this->isIdExpr($expr->name)) { if ($this->isIdExpr($expr->name)) {
$method = $this->parseIdentifier($expr->name); $method = $this->parseIdentifier($expr->name);
$this->guardAbstractMethod($parentClass, $method, $expr); $this->guardAbstractMethod($parentClass, $method, $expr);
// A private parent method is not reachable via parent:: — PHP throws
// "Call to private method" at runtime, so report it at compile time.
if ($this->getMethodFlags($parentClass, $method) & Modifiers::PRIVATE) {
$this->fatalError($expr, "Cannot access private method `{$parentClass}::{$method}()` via parent::");
}
$methodPtr = $this->getMethodPtr($parentClass, $method); $methodPtr = $this->getMethodPtr($parentClass, $method);
} else { } else {
$method = ''; $method = '';

@ -3500,6 +3500,9 @@ CODE;
if (!($flags & Modifiers::ABSTRACT)) { if (!($flags & Modifiers::ABSTRACT)) {
$this->methodDef = $this->classDef->getMethod($name); $this->methodDef = $this->classDef->getMethod($name);
// Keep the AST node so trait-composed methods can report accurate
// line numbers when validated for override compatibility later.
$this->methodDef->node = $v;
// 预处理阶段没有父类的信息,只能在实现阶段检查 // 预处理阶段没有父类的信息,只能在实现阶段检查
$this->checkParentMethodCanBeOverridden($v, $name); $this->checkParentMethodCanBeOverridden($v, $name);
$methodCodes[$name] = $this->parseFunction($v); $methodCodes[$name] = $this->parseFunction($v);
@ -3610,6 +3613,23 @@ CODE;
string $traitMethodName, string $traitMethodName,
string $classMethodName string $classMethodName
): string { ): string {
// Validate `parent::` calls emitted from this trait method against the
// parent of the class that is composing the trait. The trait itself has
// no parent at compile time, so this is the only place the parent class
// (and therefore the visibility of its methods) is known.
foreach ($methodDef->parentMethodCalls as $parentCall) {
$this->validateTraitParentCall($traitDef, $classDef, $parentCall['method'], $parentCall['node']);
}
// A trait method flattened into a class participates in the inheritance
// hierarchy: it must remain signature-compatible with any same-named
// parent method, exactly as a directly-declared override would. PHP
// enforces this at class declaration time ("Declaration of X::m() must
// be compatible with Y::m()"); without this check the incompatibility
// only surfaces as a runtime fatal error that the compiled binary would
// otherwise ignore and keep executing past.
$this->checkTraitMethodOverrideCompatibility($classDef, $methodDef, $classMethodName);
$classDef->addMethod($methodDef); $classDef->addMethod($methodDef);
$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);
@ -3643,6 +3663,88 @@ CODE;
return $code; return $code;
} }
/**
* Validate a `parent::method()` call recorded inside a trait method.
*
* The trait has no parent of its own, so the only point at which the parent
* class is known is when a class actually uses the trait. At that moment we
* can statically resolve the parent method and reject private methods, which
* PHP would otherwise only report as a runtime "Call to private method" error.
*/
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;
}
$parentClass = $usingClassDef->extends;
// Internal / not-compiled parents are opaque to the compiler; let the
// runtime enforce visibility for those.
if (!$this->hasClass($parentClass)) {
return;
}
if ($this->getMethodFlags($parentClass, $method) & Modifiers::PRIVATE) {
$this->fatalError(
$node,
"Cannot access private method `{$parentClass}::{$method}()` via parent:: in trait `{$traitDef->name}`"
);
}
}
/**
* Validate that a trait method being flattened into a class remains
* signature-compatible with any same-named method declared up the parent
* chain — the same compatibility contract a directly-declared override must
* satisfy (see `checkParentMethodCanBeOverridden`).
*
* Only the signature contract is enforced here (not the "cannot override
* private/final" rule), because a trait method is flattened into the class
* and, like a normal subclass method, is allowed to shadow a private parent
* method. PHP reports the incompatibility as a class-declaration fatal error
* ("Declaration of X::m() must be compatible with Y::m()"), which we surface
* at compile time so the broken program is rejected instead of being emitted
* and executed past a runtime fatal error.
*/
private function checkTraitMethodOverrideCompatibility(ClassDef $usingClassDef, MethodDef $methodDef, string $methodName): void
{
if ($methodName === '__construct' || $methodDef->node === null) {
return;
}
$classDef = $usingClassDef;
while (true) {
$extends = $classDef->extends;
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)) {
break;
}
$classDef = $this->getClass($extends);
if ($classDef->hasMethod($methodName)) {
$this->validateMethodOverrideSignature(
$methodDef->node,
$methodName,
$methodDef,
$classDef->getMethod($methodName),
$extends
);
break;
}
if ($classDef->hasAbstractMethod($methodName) && isset($classDef->abstractMethodDefs[strtolower($methodName)])) {
$this->validateMethodOverrideSignature(
$methodDef->node,
$methodName,
$methodDef,
$classDef->getAbstractMethod($methodName),
$extends
);
break;
}
}
}
private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool
{ {
return $existing->flags === $incoming->flags && return $existing->flags === $incoming->flags &&

@ -0,0 +1,36 @@
--TEST--
Trait constructor calling parent::__construct of the composing class
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
public function __construct(int $value)
{
parent::__construct($value, true);
}
}
class ParentClass
{
public function __construct(int $value, bool $bool)
{
var_dump($value, $bool);
}
}
class TestClass extends ParentClass
{
use TestTrait;
}
function main()
{
new TestClass(123);
}
?>
--EXPECT--
int(123)
bool(true)

@ -0,0 +1,34 @@
--TEST--
Trait method calling protected parent::method() of the composing class
--FILE--
<?php
trait GreetTrait
{
public function greet(string $name): string
{
return parent::greet($name) . ' [via trait]';
}
}
class BaseGreeter
{
protected function greet(string $name): string
{
return 'Hello ' . $name;
}
}
class ChildGreeter extends BaseGreeter
{
use GreetTrait;
}
function main(): void
{
$g = new ChildGreeter();
var_dump($g->greet('World'));
}
?>
--EXPECT--
string(23) "Hello World [via trait]"

@ -0,0 +1,34 @@
--TEST--
Trait method calling parent::method() of the composing class
--FILE--
<?php
trait GreetTrait
{
public function greet(string $name): string
{
return parent::greet($name) . ' [via trait]';
}
}
class BaseGreeter
{
public function greet(string $name): string
{
return 'Hello ' . $name;
}
}
class ChildGreeter extends BaseGreeter
{
use GreetTrait;
}
function main(): void
{
$g = new ChildGreeter();
var_dump($g->greet('World'));
}
?>
--EXPECT--
string(23) "Hello World [via trait]"
Loading…
Cancel
Save