fix(trait): preserve cross-instance method scope (fixes #67)

master
韩天峰 11 hours ago
parent 24924673ec
commit 456f1a38b2
  1. 49
      src/Translator.php
  2. 53
      tests/compiler/trait/trait-private-nested-alias-cross-instance.phpt
  3. 57
      tests/compiler/trait/trait-protected-cross-instance.phpt

@ -4057,6 +4057,26 @@ CODE;
$methodCodes = [];
// Trait methods are flattened into the consuming class and therefore
// participate in that class's lexical visibility. Install every
// composed declaration before lowering any method body: a class method
// may call a protected/private trait method on another instance, and
// runtime dispatch needs to know that the call carries class scope.
$composedTraitMethods = [];
if ($composedClass !== null) {
foreach ($composedClass->stmts as $stmt) {
if (!$stmt instanceof Node\Stmt\ClassMethod
|| !is_string($stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE))) {
continue;
}
$origin = (string) $stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE);
$this->withTraitNameContext($origin, function () use ($stmt): void {
$this->installComposedTraitMethod($stmt);
});
$composedTraitMethods[] = [$stmt, $origin];
}
}
foreach ($class->stmts as $v) {
$type = $v->getType();
switch ($type) {
@ -4078,28 +4098,13 @@ CODE;
break;
}
}
if ($composedClass !== null) {
$composedTraitMethods = [];
foreach ($composedClass->stmts as $stmt) {
if (!$stmt instanceof Node\Stmt\ClassMethod
|| !is_string($stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE))) {
continue;
}
$origin = (string) $stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE);
$this->withTraitNameContext($origin, function () use ($stmt): void {
$this->installComposedTraitMethod($stmt);
});
$composedTraitMethods[] = [$stmt, $origin];
}
// Every method must be visible before any body is lowered. Trait
// methods may call a private helper declared later in the same
// trait; compiling as we install would incorrectly lower that call
// as a dynamic callback instead of a native class method call.
foreach ($composedTraitMethods as [$stmt, $origin]) {
$this->withTraitNameContext($origin, function () use ($stmt, &$methodCodes): void {
$this->parseClassMethod($stmt, $methodCodes);
});
}
// All composed declarations are now visible. Lower their bodies in a
// separate pass so one trait method can call another method declared
// later in the same or a nested trait.
foreach ($composedTraitMethods as [$stmt, $origin]) {
$this->withTraitNameContext($origin, function () use ($stmt, &$methodCodes): void {
$this->parseClassMethod($stmt, $methodCodes);
});
}
if (!$class instanceof Node\Stmt\Trait_) {
$this->validateOverrideAttributes($class);

@ -0,0 +1,53 @@
--TEST--
Private, nested and aliased trait methods retain class scope across instances
--FILE--
<?php
trait NestedPrivateMethod
{
private function nestedSecret(): string
{
return 'nested';
}
}
trait NestedPrivateComposition
{
use NestedPrivateMethod;
}
trait AliasMethodSource
{
protected function sourceSecret(): string
{
return 'alias';
}
}
class PrivateTraitConsumer
{
use NestedPrivateComposition;
use AliasMethodSource {
sourceSecret as private aliasSecret;
}
public function readOther(): string
{
$other = new static();
return $other->nestedSecret() . ':' . $other->aliasSecret();
}
}
class PrivateTraitChild extends PrivateTraitConsumer
{
}
function main(): void
{
echo (new PrivateTraitConsumer())->readOther(), "\n";
echo (new PrivateTraitChild())->readOther(), "\n";
}
?>
--EXPECT--
nested:alias
nested:alias

@ -0,0 +1,57 @@
--TEST--
Trait-composed protected methods retain class scope across instances
--FILE--
<?php
trait CrossInstanceEvents
{
protected function fire(string $event): string
{
return 'base:' . $event;
}
protected function marker(): string
{
return 'marker';
}
}
class CrossInstanceBase
{
use CrossInstanceEvents;
public function touchSelf(): string
{
$other = new self();
return $other->marker() . ':' . $other->fire('self');
}
public function touchStatic(): string
{
$other = new static();
return $other->marker() . ':' . $other->fire('static');
}
}
class CrossInstanceChild extends CrossInstanceBase
{
protected function fire(string $event): string
{
return 'child:' . $event;
}
}
function main(): void
{
$base = new CrossInstanceBase();
echo $base->touchSelf(), "\n";
echo $base->touchStatic(), "\n";
$child = new CrossInstanceChild();
echo $child->touchStatic(), "\n";
}
?>
--EXPECT--
marker:base:self
marker:base:static
marker:child:static
Loading…
Cancel
Save