fix: preserve dispatch for trait method overrides

master
韩天峰 2 days ago
parent 90f2dd06f6
commit d39bd7fdbd
  1. 2
      src/Entity/ClassDef.php
  2. 59
      src/Preprocessor.php
  3. 103
      tests/compiler/trait/trait-overrides-inherited-dispatch.phpt

@ -76,6 +76,8 @@ class ClassDef extends ClassLikeDef
public array $traitUseFunctions = [];
/** @var array<string, string> */
public array $traitUseConstants = [];
/** @var list<string> Traits directly used by this class or trait. */
public array $usedTraits = [];
/**
* FullMethodName -> alias list

@ -2387,6 +2387,26 @@ class Preprocessor extends CompilerBase
return;
}
$this->methodOverrideFlagsFinalized = true;
// Trait composition introduces real methods into the consuming class.
// They participate in virtual dispatch exactly like methods declared
// in the class body, so mark them before any method body is lowered.
// This is deliberately conservative: an extra mark only disables a
// native direct-call optimization, while a missing mark bypasses the
// trait override at runtime.
foreach ($this->symbols->classes() as $classDef) {
if ($classDef->trait !== null || $classDef->usedTraits === []) {
continue;
}
$traitMethods = [];
$visitedTraits = [];
$this->collectComposedTraitMethodNames($classDef, $traitMethods, $visitedTraits);
$className = strtolower($classDef->getNamespacedName(false));
foreach (array_keys($traitMethods) as $method) {
$this->classMethodOverride[$className . '::' . $method] ??= false;
}
}
foreach (array_keys($this->classMethodOverride) as $fullMethodNameLower) {
$pos = strrpos($fullMethodNameLower, '::');
if ($pos === false) {
@ -2404,6 +2424,44 @@ class Preprocessor extends CompilerBase
}
}
/**
* Collect every concrete method a class may receive through direct or
* nested trait composition. Conflict suppression may make this set larger
* than the final method table; those false positives safely retain Zend
* dynamic dispatch.
*
* @param array<string, true> $methods
* @param array<string, true> $visitedTraits
*/
private function collectComposedTraitMethodNames(
ClassDef $owner,
array &$methods,
array &$visitedTraits,
): void {
foreach ($owner->usedTraits as $traitName) {
$traitKey = strtolower($traitName);
if (isset($visitedTraits[$traitKey]) || !$this->hasClass($traitName)) {
continue;
}
$visitedTraits[$traitKey] = true;
$traitDef = $this->getClass($traitName);
if ($traitDef->trait === null) {
continue;
}
foreach ($traitDef->methods as $method) {
if (!($method->flags & Modifiers::ABSTRACT)) {
$methods[strtolower($method->name)] = true;
}
}
foreach ($owner->traitAliases as $aliases) {
foreach ($aliases as $alias) {
$methods[strtolower($alias['newName'])] = true;
}
}
$this->collectComposedTraitMethodNames($traitDef, $methods, $visitedTraits);
}
}
private function assertKeywordMethodMayBeDeclared(
Node\Stmt\ClassMethod $method,
string $name,
@ -2741,6 +2799,7 @@ class Preprocessor extends CompilerBase
}
foreach ($v->traits as $trait) {
$traitName = $this->getNamespacedClassName($this->parseIdentifier($trait));
$this->classDef->usedTraits[] = $traitName;
if (!$this->isInternalClass($traitName)) {
$this->symbolCallInFile[$this->file][] = strtolower($traitName);
}

@ -0,0 +1,103 @@
--TEST--
Trait-composed methods override inherited methods in virtual dispatch
--FILE--
<?php
class DispatchBase
{
public function dispatch(): string
{
return $this->perform();
}
protected function perform(): string
{
return 'base';
}
}
trait DirectOverride
{
protected function perform(): string
{
return 'direct-trait';
}
}
final class DirectConsumer extends DispatchBase
{
use DirectOverride;
}
trait NestedOverride
{
protected function perform(): string
{
return 'nested-trait';
}
}
trait NestedComposition
{
use NestedOverride;
}
final class NestedConsumer extends DispatchBase
{
use NestedComposition;
}
trait AliasedOverride
{
protected function replacement(): string
{
return 'aliased-trait';
}
}
final class AliasedConsumer extends DispatchBase
{
use AliasedOverride {
replacement as perform;
}
}
class PrivateDispatchBase
{
public function dispatch(): string
{
return $this->perform();
}
private function perform(): string
{
return 'private-base';
}
}
trait PrivateNameCollision
{
protected function perform(): string
{
return 'trait';
}
}
final class PrivateConsumer extends PrivateDispatchBase
{
use PrivateNameCollision;
}
function main(): void
{
var_dump((new DirectConsumer())->dispatch());
var_dump((new NestedConsumer())->dispatch());
var_dump((new AliasedConsumer())->dispatch());
var_dump((new PrivateConsumer())->dispatch());
}
?>
--EXPECT--
string(12) "direct-trait"
string(12) "nested-trait"
string(13) "aliased-trait"
string(12) "private-base"
Loading…
Cancel
Save