From 456f1a38b2f810afba5f275c9477bd757c9f5c9d Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Wed, 2 Sep 2026 08:22:52 +0800 Subject: [PATCH] fix(trait): preserve cross-instance method scope (fixes #67) --- src/Translator.php | 49 +++++++++------- ...t-private-nested-alias-cross-instance.phpt | 53 +++++++++++++++++ .../trait/trait-protected-cross-instance.phpt | 57 +++++++++++++++++++ 3 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 tests/compiler/trait/trait-private-nested-alias-cross-instance.phpt create mode 100644 tests/compiler/trait/trait-protected-cross-instance.phpt diff --git a/src/Translator.php b/src/Translator.php index f9cf7073..7746bbae 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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); diff --git a/tests/compiler/trait/trait-private-nested-alias-cross-instance.phpt b/tests/compiler/trait/trait-private-nested-alias-cross-instance.phpt new file mode 100644 index 00000000..2509c281 --- /dev/null +++ b/tests/compiler/trait/trait-private-nested-alias-cross-instance.phpt @@ -0,0 +1,53 @@ +--TEST-- +Private, nested and aliased trait methods retain class scope across instances +--FILE-- +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 diff --git a/tests/compiler/trait/trait-protected-cross-instance.phpt b/tests/compiler/trait/trait-protected-cross-instance.phpt new file mode 100644 index 00000000..4e73a366 --- /dev/null +++ b/tests/compiler/trait/trait-protected-cross-instance.phpt @@ -0,0 +1,57 @@ +--TEST-- +Trait-composed protected methods retain class scope across instances +--FILE-- +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