diff --git a/src/Translator.php b/src/Translator.php index 57fb2b8a..fe561c62 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3128,13 +3128,13 @@ public function composeTraitAst(Node\Stmt\ClassLike $stmt, Node\Name $className) $aliasName = strtolower($alias['newName']); if ($aliasName === $methodName) { if ($alias['newModifier']) { - $traitStmt->flags = $alias['newModifier']; + $traitStmt->flags = $this->applyTraitAliasModifier($traitStmt->flags, $alias['newModifier']); } } elseif (!isset($methods[$aliasName]) && !isset($traitMethods[$aliasName])) { $aliasStmt = clone $traitStmt; $aliasStmt->name = new Node\Identifier($alias['newName']); if ($alias['newModifier']) { - $aliasStmt->flags = $alias['newModifier']; + $aliasStmt->flags = $this->applyTraitAliasModifier($aliasStmt->flags, $alias['newModifier']); } $aliasStmts[] = $aliasStmt; $traitMethods[$aliasName] = [$traitFullName, $aliasStmt]; @@ -3144,6 +3144,13 @@ public function composeTraitAst(Node\Stmt\ClassLike $stmt, Node\Name $className) unset($traitStmts[$k1]); continue; } + if (isset($methods[$methodName])) { + // The class's own method always wins: suppressed + // trait copies must not take part in trait-vs-trait + // conflict resolution. + unset($traitStmts[$k1]); + continue; + } if (isset($traitMethods[$methodName])) { [$existingTraitName, $existingStmt] = $traitMethods[$methodName]; $newAbstract = $traitStmt->isAbstract(); @@ -3167,18 +3174,26 @@ public function composeTraitAst(Node\Stmt\ClassLike $stmt, Node\Name $className) } if (!$newAbstract && $existingAbstract) { - // New concrete replaces existing abstract + // The new concrete method fulfills the abstract + // requirement: drop the already-collected + // abstract declaration and keep this one. + foreach ($stmt->stmts as $k3 => $mergedStmt) { + if ($mergedStmt === $existingStmt) { + unset($stmt->stmts[$k3]); + } + } + foreach ($aliasStmts as $k3 => $pendingAliasStmt) { + if ($pendingAliasStmt === $existingStmt) { + unset($aliasStmts[$k3]); + } + } $traitMethods[$methodName] = [$traitFullName, $traitStmt]; - unset($traitStmts[$k1]); continue; } // Both concrete — error $this->fatalError($classStmt, "Trait `{$traitFullName}` method `{$methodName}` already exists"); } - if (isset($methods[$methodName])) { - unset($traitStmts[$k1]); - } $traitMethods[$methodName] = [$traitFullName, $traitStmt]; } if ($traitStmt instanceof Node\Stmt\ClassConst) { @@ -3243,6 +3258,20 @@ public function composeTraitAst(Node\Stmt\ClassLike $stmt, Node\Name $className) } + /** + * Apply a trait alias modifier the way PHP does: a new visibility replaces + * only the visibility bits, and every other flag (static, final, abstract) + * is kept. A modifier without visibility (e.g. `as final`) keeps the + * original visibility. + */ + private function applyTraitAliasModifier(int $flags, int $newModifier): int + { + if ($newModifier & Modifiers::VISIBILITY_MASK) { + $flags &= ~Modifiers::VISIBILITY_MASK; + } + return $flags | $newModifier; + } + private function cloneAstNode(Node $node): Node { $traverser = new NodeTraverser(); diff --git a/tests/compiler/trait/trait-composition-precedence.phpt b/tests/compiler/trait/trait-composition-precedence.phpt new file mode 100644 index 00000000..28f38b94 --- /dev/null +++ b/tests/compiler/trait/trait-composition-precedence.phpt @@ -0,0 +1,51 @@ +--TEST-- +Trait composition: concrete fulfills abstract, alias keeps static, class method wins +--FILE-- +name(), "\n"; + echo (new ConcreteFirst())->name(), "\n"; + echo Factory::build(), "\n"; + echo Stats::total(), "\n"; + echo (new Self1())->who(), "\n"; +} +?> +--EXPECT-- +HasName +HasName +made +7 +Self1