Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions src/Translator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down
51 changes: 51 additions & 0 deletions tests/compiler/trait/trait-composition-precedence.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
--TEST--
Trait composition: concrete fulfills abstract, alias keeps static, class method wins
--FILE--
<?php

// A concrete trait method fulfills an abstract requirement from another
// trait, regardless of the order the traits are listed in.
trait NeedsName { abstract public function name(): string; }
trait HasName { public function name(): string { return "HasName"; } }
class AbstractFirst { use NeedsName, HasName; }
class ConcreteFirst { use HasName, NeedsName; }

// An alias visibility change keeps the `static` flag.
trait Maker {
public static function make(): string { return "made"; }
}
class Factory {
use Maker { make as protected; }
public static function build(): string { return static::make(); }
}

// An alias under a new name keeps the `static` flag too.
trait Counter {
public static function count7(): int { return 7; }
}
class Stats {
use Counter { count7 as protected seven; }
public static function total(): int { return static::seven(); }
}

// The class's own method wins over two same-name trait methods without
// this counting as a trait-vs-trait conflict.
trait WhoA { public function who(): string { return "WhoA"; } }
trait WhoB { public function who(): string { return "WhoB"; } }
class Self1 { use WhoA, WhoB; public function who(): string { return "Self1"; } }

function main(): void
{
echo (new AbstractFirst())->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
Loading