From d39bd7fdbd599983386f32c1279e7a3643520256 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 3 Sep 2026 13:53:30 +0800 Subject: [PATCH] fix: preserve dispatch for trait method overrides --- src/Entity/ClassDef.php | 2 + src/Preprocessor.php | 59 ++++++++++ .../trait-overrides-inherited-dispatch.phpt | 103 ++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 tests/compiler/trait/trait-overrides-inherited-dispatch.phpt diff --git a/src/Entity/ClassDef.php b/src/Entity/ClassDef.php index 5d9bb163..f62a4dde 100644 --- a/src/Entity/ClassDef.php +++ b/src/Entity/ClassDef.php @@ -76,6 +76,8 @@ class ClassDef extends ClassLikeDef public array $traitUseFunctions = []; /** @var array */ public array $traitUseConstants = []; + /** @var list Traits directly used by this class or trait. */ + public array $usedTraits = []; /** * FullMethodName -> alias list diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 4a1efcd6..389f5107 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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 $methods + * @param array $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); } diff --git a/tests/compiler/trait/trait-overrides-inherited-dispatch.phpt b/tests/compiler/trait/trait-overrides-inherited-dispatch.phpt new file mode 100644 index 00000000..60c70006 --- /dev/null +++ b/tests/compiler/trait/trait-overrides-inherited-dispatch.phpt @@ -0,0 +1,103 @@ +--TEST-- +Trait-composed methods override inherited methods in virtual dispatch +--FILE-- +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"