fix(translator): correct trait composition precedence and alias modifiers

Three defects in composeTraitAst diverged from PHP trait semantics:

1. When a later trait supplied a concrete method for a name an earlier
   trait declared abstract, the branch unset the new concrete statement
   while the already-merged abstract one stayed in the class AST. The
   composed class kept only the abstract declaration, so instantiating
   it failed with "Cannot instantiate abstract class" even though the
   method body existed. The reverse trait order worked. Now the merged
   abstract declaration is dropped and the concrete method is kept.

2. Alias adaptations assigned the new modifier over the whole flag set,
   wiping static/final/abstract: `use Maker { make as protected; }`
   turned a static method into an instance method, and static:: calls
   on it miscompiled. PHP replaces only the visibility bits (and keeps
   the original visibility when the modifier carries none, e.g.
   `as final`).

3. A method defined by the class itself did not suppress the
   trait-vs-trait conflict check, so `class C { use A, B; function f(){} }`
   with f() in both traits died with a spurious "method already exists"
   fatal. The class-method check now runs before conflict resolution and
   suppressed trait copies are no longer registered as trait methods.
master
Alessio Giacobbe 5 days ago
parent b493ac79c5
commit a0be8bf349
No known key found for this signature in database
  1. 43
      src/Translator.php
  2. 51
      tests/compiler/trait/trait-composition-precedence.phpt

@ -3128,13 +3128,13 @@ CODE;
$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 @@ CODE;
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 @@ CODE;
}
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 @@ CODE;
}
/**
* 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();

@ -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…
Cancel
Save