From 9d22e8ca246f6336a71735d0a94d9d36b823d3c6 Mon Sep 17 00:00:00 2001 From: Yurun Date: Sun, 19 Jul 2026 09:03:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dtrait=E4=B8=ADparent::?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=8F=8A=E6=96=B9=E6=B3=95=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=80=A7=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpunit/code/parent-method-private.php | 23 ++++ .../trait-method-override-incompatible.php | 24 +++++ phpunit/code/trait-parent-method-private.php | 28 +++++ .../code/trait-parent-method-protected.php | 28 +++++ phpunit/src/ClassTest.php | 17 +++ phpunit/src/InheritanceErrorTest.php | 9 ++ src/Entity/MethodDef.php | 16 +++ src/Parser/MethodCallTrait.php | 26 +++++ src/Translator.php | 102 ++++++++++++++++++ .../trait/trait-parent-constructor.phpt | 36 +++++++ .../trait/trait-parent-method-protected.phpt | 34 ++++++ tests/compiler/trait/trait-parent-method.phpt | 34 ++++++ 12 files changed, 377 insertions(+) create mode 100644 phpunit/code/parent-method-private.php create mode 100644 phpunit/code/trait-method-override-incompatible.php create mode 100644 phpunit/code/trait-parent-method-private.php create mode 100644 phpunit/code/trait-parent-method-protected.php create mode 100644 tests/compiler/trait/trait-parent-constructor.phpt create mode 100644 tests/compiler/trait/trait-parent-method-protected.phpt create mode 100644 tests/compiler/trait/trait-parent-method.phpt diff --git a/phpunit/code/parent-method-private.php b/phpunit/code/parent-method-private.php new file mode 100644 index 00000000..cd030d1e --- /dev/null +++ b/phpunit/code/parent-method-private.php @@ -0,0 +1,23 @@ +reveal()); +} diff --git a/phpunit/code/trait-method-override-incompatible.php b/phpunit/code/trait-method-override-incompatible.php new file mode 100644 index 00000000..eb4de957 --- /dev/null +++ b/phpunit/code/trait-method-override-incompatible.php @@ -0,0 +1,24 @@ +reveal()); +} diff --git a/phpunit/code/trait-parent-method-protected.php b/phpunit/code/trait-parent-method-protected.php new file mode 100644 index 00000000..1a94d8b9 --- /dev/null +++ b/phpunit/code/trait-parent-method-protected.php @@ -0,0 +1,28 @@ +greet('World')); +} diff --git a/phpunit/src/ClassTest.php b/phpunit/src/ClassTest.php index a2c5de87..96c1e4a9 100644 --- a/phpunit/src/ClassTest.php +++ b/phpunit/src/ClassTest.php @@ -37,6 +37,23 @@ class ClassTest extends \BaseTest $this->exec('Cannot override private method `Base::doWork()`', 'override-private-method.php'); } + public function testTraitMayCallProtectedParentMethod() + { + // A protected parent method is reachable via parent:: from a trait, + // matching PHP runtime behaviour. + $this->compile('trait-parent-method-protected.php'); + } + + public function testCannotAccessPrivateParentMethodFromRegularClass() + { + $this->exec('Cannot access private method `Base::secret()`', 'parent-method-private.php'); + } + + public function testCannotAccessPrivateParentMethodFromTrait() + { + $this->exec('Cannot access private method `BaseSecret::secret()`', 'trait-parent-method-private.php'); + } + public function testSelfCanBePartOfUnionType() { global $translator; diff --git a/phpunit/src/InheritanceErrorTest.php b/phpunit/src/InheritanceErrorTest.php index 6c537f0c..18447b7d 100644 --- a/phpunit/src/InheritanceErrorTest.php +++ b/phpunit/src/InheritanceErrorTest.php @@ -306,4 +306,13 @@ class InheritanceErrorTest extends TestCase { $this->exec('must be compatible', 'abstract_method_signature_mismatch.php'); } + + public function testTraitMethodMustBeCompatibleWithParent() + { + // A trait method flattened into a class must remain signature-compatible + // with any same-named parent method, just like a directly-declared + // override. Without this check the incompatibility only surfaces as a + // runtime fatal error that the compiled binary would otherwise ignore. + $this->exec('must be compatible', 'trait-method-override-incompatible.php'); + } } diff --git a/src/Entity/MethodDef.php b/src/Entity/MethodDef.php index 41bbecba..3973e385 100644 --- a/src/Entity/MethodDef.php +++ b/src/Entity/MethodDef.php @@ -15,6 +15,22 @@ class MethodDef public ?FunctionDef $functionDef = null; public bool $hasDynamicCall = false; + /** + * The original `ClassMethod` AST node this definition was parsed from. + * Stored so that later validation (e.g. trait method override compatibility + * checks performed at the `use` site) can report accurate line information. + */ + public ?\PhpParser\Node\Stmt\ClassMethod $node = null; + + /** + * For methods defined inside a trait, records `parent::method()` calls so + * the compiler can validate their visibility against the parent of each + * class that uses the trait (the trait itself has no parent at compile time). + * + * @var array + */ + public array $parentMethodCalls = []; + public function __construct(int $flags, string $name) { $this->flags = $flags; diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 8b86e45c..57e21d57 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -243,6 +243,27 @@ trait MethodCallTrait protected function parseParentMethodCall(Expr\StaticCall $expr): string { + // Inside a trait, `parent::` refers to the parent of the class that + // *uses* the trait. That parent class is only known at runtime (a single + // trait may be composed into classes with different parents), so resolve + // it dynamically from the current object's class entry instead of the + // trait's own (non-existent) parent. + if ($this->classDef !== null && $this->classDef->trait !== null) { + $method = $this->isIdExpr($expr->name) ? $this->parseIdentifier($expr->name) : ''; + // Record the parent:: call so it can be validated against the parent + // of every class that uses this trait (the trait itself has no parent + // at compile time). Dynamic method names cannot be validated statically. + if ($method !== '' && isset($this->methodDef)) { + $this->methodDef->parentMethodCalls[] = ['method' => $method, 'node' => $expr]; + } + $methodPtr = 'php::getMethod(this_.parent_ce(), ' . $this->identifierToStr($expr->name) . ')'; + if (empty($expr->args)) { + return 'this_.call(' . $methodPtr . ')'; + } + // Parent class is unknown statically, so by-ref argument detection is skipped. + return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs($expr->args, $method, '') . ')'; + } + if (!$this->classDef->extends) { $this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class'); } @@ -250,6 +271,11 @@ trait MethodCallTrait if ($this->isIdExpr($expr->name)) { $method = $this->parseIdentifier($expr->name); $this->guardAbstractMethod($parentClass, $method, $expr); + // A private parent method is not reachable via parent:: — PHP throws + // "Call to private method" at runtime, so report it at compile time. + if ($this->getMethodFlags($parentClass, $method) & Modifiers::PRIVATE) { + $this->fatalError($expr, "Cannot access private method `{$parentClass}::{$method}()` via parent::"); + } $methodPtr = $this->getMethodPtr($parentClass, $method); } else { $method = ''; diff --git a/src/Translator.php b/src/Translator.php index 676618a3..33d07f86 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -3500,6 +3500,9 @@ CODE; if (!($flags & Modifiers::ABSTRACT)) { $this->methodDef = $this->classDef->getMethod($name); + // Keep the AST node so trait-composed methods can report accurate + // line numbers when validated for override compatibility later. + $this->methodDef->node = $v; // 预处理阶段没有父类的信息,只能在实现阶段检查 $this->checkParentMethodCanBeOverridden($v, $name); $methodCodes[$name] = $this->parseFunction($v); @@ -3610,6 +3613,23 @@ CODE; string $traitMethodName, string $classMethodName ): string { + // Validate `parent::` calls emitted from this trait method against the + // parent of the class that is composing the trait. The trait itself has + // no parent at compile time, so this is the only place the parent class + // (and therefore the visibility of its methods) is known. + foreach ($methodDef->parentMethodCalls as $parentCall) { + $this->validateTraitParentCall($traitDef, $classDef, $parentCall['method'], $parentCall['node']); + } + + // A trait method flattened into a class participates in the inheritance + // hierarchy: it must remain signature-compatible with any same-named + // parent method, exactly as a directly-declared override would. PHP + // enforces this at class declaration time ("Declaration of X::m() must + // be compatible with Y::m()"); without this check the incompatibility + // only surfaces as a runtime fatal error that the compiled binary would + // otherwise ignore and keep executing past. + $this->checkTraitMethodOverrideCompatibility($classDef, $methodDef, $classMethodName); + $classDef->addMethod($methodDef); $traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name); $classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name); @@ -3643,6 +3663,88 @@ CODE; return $code; } + /** + * Validate a `parent::method()` call recorded inside a trait method. + * + * The trait has no parent of its own, so the only point at which the parent + * class is known is when a class actually uses the trait. At that moment we + * can statically resolve the parent method and reject private methods, which + * PHP would otherwise only report as a runtime "Call to private method" error. + */ + private function validateTraitParentCall(ClassDef $traitDef, ClassDef $usingClassDef, string $method, NodeAbstract $node): void + { + if (!$usingClassDef->extends) { + // No parent class: cannot validate; PHP would report at runtime. + return; + } + $parentClass = $usingClassDef->extends; + // Internal / not-compiled parents are opaque to the compiler; let the + // runtime enforce visibility for those. + if (!$this->hasClass($parentClass)) { + return; + } + if ($this->getMethodFlags($parentClass, $method) & Modifiers::PRIVATE) { + $this->fatalError( + $node, + "Cannot access private method `{$parentClass}::{$method}()` via parent:: in trait `{$traitDef->name}`" + ); + } + } + + /** + * Validate that a trait method being flattened into a class remains + * signature-compatible with any same-named method declared up the parent + * chain — the same compatibility contract a directly-declared override must + * satisfy (see `checkParentMethodCanBeOverridden`). + * + * Only the signature contract is enforced here (not the "cannot override + * private/final" rule), because a trait method is flattened into the class + * and, like a normal subclass method, is allowed to shadow a private parent + * method. PHP reports the incompatibility as a class-declaration fatal error + * ("Declaration of X::m() must be compatible with Y::m()"), which we surface + * at compile time so the broken program is rejected instead of being emitted + * and executed past a runtime fatal error. + */ + private function checkTraitMethodOverrideCompatibility(ClassDef $usingClassDef, MethodDef $methodDef, string $methodName): void + { + if ($methodName === '__construct' || $methodDef->node === null) { + return; + } + $classDef = $usingClassDef; + while (true) { + $extends = $classDef->extends; + if (!$extends) { + break; + } + // Internal / not-compiled parents are opaque to the compiler; let the + // runtime enforce compatibility for those. + if ($classDef->inheritedFromInternalClass || !$this->hasClass($extends)) { + break; + } + $classDef = $this->getClass($extends); + if ($classDef->hasMethod($methodName)) { + $this->validateMethodOverrideSignature( + $methodDef->node, + $methodName, + $methodDef, + $classDef->getMethod($methodName), + $extends + ); + break; + } + if ($classDef->hasAbstractMethod($methodName) && isset($classDef->abstractMethodDefs[strtolower($methodName)])) { + $this->validateMethodOverrideSignature( + $methodDef->node, + $methodName, + $methodDef, + $classDef->getAbstractMethod($methodName), + $extends + ); + break; + } + } + } + private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool { return $existing->flags === $incoming->flags && diff --git a/tests/compiler/trait/trait-parent-constructor.phpt b/tests/compiler/trait/trait-parent-constructor.phpt new file mode 100644 index 00000000..941968a6 --- /dev/null +++ b/tests/compiler/trait/trait-parent-constructor.phpt @@ -0,0 +1,36 @@ +--TEST-- +Trait constructor calling parent::__construct of the composing class +--FILE-- + +--EXPECT-- +int(123) +bool(true) diff --git a/tests/compiler/trait/trait-parent-method-protected.phpt b/tests/compiler/trait/trait-parent-method-protected.phpt new file mode 100644 index 00000000..661ab30c --- /dev/null +++ b/tests/compiler/trait/trait-parent-method-protected.phpt @@ -0,0 +1,34 @@ +--TEST-- +Trait method calling protected parent::method() of the composing class +--FILE-- +greet('World')); +} +?> +--EXPECT-- +string(23) "Hello World [via trait]" diff --git a/tests/compiler/trait/trait-parent-method.phpt b/tests/compiler/trait/trait-parent-method.phpt new file mode 100644 index 00000000..02e4839b --- /dev/null +++ b/tests/compiler/trait/trait-parent-method.phpt @@ -0,0 +1,34 @@ +--TEST-- +Trait method calling parent::method() of the composing class +--FILE-- +greet('World')); +} +?> +--EXPECT-- +string(23) "Hello World [via trait]"