From 7be7240d5d0b8ce043fb9bc7f030acb62d18867e Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:40:29 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(translator):=20=E4=BF=AE=E5=A4=8Dtrait?= =?UTF-8?q?=20parent::=E8=B0=83=E7=94=A8=E7=9A=84=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E5=8F=82=E6=95=B0=E4=B8=8D=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trait-aliased-constructor-parent-call.php | 37 ++++++++++++++++ phpunit/src/TraitFuncDeclTest.php | 39 ++++++++++++++++ src/Entity/FunctionDef.php | 8 ++++ src/Translator.php | 44 +++++++++---------- 4 files changed, 106 insertions(+), 22 deletions(-) create mode 100644 phpunit/code/trait-aliased-constructor-parent-call.php create mode 100644 phpunit/src/TraitFuncDeclTest.php diff --git a/phpunit/code/trait-aliased-constructor-parent-call.php b/phpunit/code/trait-aliased-constructor-parent-call.php new file mode 100644 index 00000000..db77562c --- /dev/null +++ b/phpunit/code/trait-aliased-constructor-parent-call.php @@ -0,0 +1,37 @@ + $v) { + echo ' ' . $k . '=' . $v; + } + echo "\n"; + } +} + +trait TPdoDriver +{ + public function __construct(array $option = []) + { + $option['fromTrait'] = 1; + parent::__construct($option); + } +} + +class Driver extends Base +{ + use TPdoDriver { + __construct as private tPdoDriverConstruct; + } + + public function __construct(array $option = []) + { + $option['username'] = 'postgres'; + $this->tPdoDriverConstruct($option); + } +} diff --git a/phpunit/src/TraitFuncDeclTest.php b/phpunit/src/TraitFuncDeclTest.php new file mode 100644 index 00000000..fd06987f --- /dev/null +++ b/phpunit/src/TraitFuncDeclTest.php @@ -0,0 +1,39 @@ +compile('trait-aliased-constructor-parent-call.php'); + + global $translator; + $compiler = $translator; + + $headerPath = $compiler->getIncludeDir() . '/php_trait_parent_ce_func_decl.h'; + if (file_exists($headerPath)) { + @unlink($headerPath); + } + // Emit the shared function-declaration header (genFunctionDeclarations), which + // is what the fix targets. + $compiler->genFunctionDeclarations($headerPath); + + $decl = file_get_contents($headerPath); + $this->assertStringContainsString( + 'trait_parent_ce', + $decl, + 'func_decl.h must declare the implicit trait_parent_ce parameter for trait methods with parent:: calls' + ); + } +} diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 78e8c01b..89221245 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -33,6 +33,14 @@ class FunctionDef public string $attributeFactoryScope = ''; /** External library imported by the stub containing this function. */ public string $importLibrary = ''; + /** + * True for a trait method whose body contains `parent::` calls. Such methods + * receive an implicit `zend_class_entry *trait_parent_ce` parameter (right after + * `this_`) so the `parent::` call can be bound to the class that composes the + * trait. Both the definition and the shared `func_decl.h` declaration must emit + * this parameter, otherwise the declaration/definition signatures disagree. + */ + public bool $traitParentCe = false; public bool $returnTypeUndeclared = false; public bool $returnsByRef = false; public bool $generator = false; diff --git a/src/Translator.php b/src/Translator.php index 14916aec..4903cc57 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1602,6 +1602,12 @@ CODE; $list = []; if ($func->method) { $list[] = Type::OBJECT . ' &this_'; + // A trait method with `parent::` calls receives an implicit + // `trait_parent_ce` parameter right after `this_`. The definition + // adds it (see parseFunction); the declaration must match. + if ($func->traitParentCe) { + $list[] = 'zend_class_entry *trait_parent_ce'; + } } $argInfoList = $func->argInfoList; if ($argInfoList) { @@ -3464,6 +3470,9 @@ CODE; $functionDeclCode .= Type::OBJECT . ' &this_'; if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) { $functionDeclCode .= ', zend_class_entry *trait_parent_ce'; + // Record the implicit parameter so the shared `func_decl.h` + // declaration (genFunctionDeclaration) emits the same signature. + $this->functionDef->traitParentCe = true; } if ($this->functionDef->params) { $functionDeclCode .= ', '; @@ -4320,6 +4329,14 @@ CODE; // function untouched. $this->reresolveTraitLateBoundTypes($classDef, $methodDef); + // The wrapper is a method of the *composing* class, not a trait method, so + // it must not receive the implicit `trait_parent_ce` parameter (it computes + // the parent class entry itself when forwarding to the trait function). The + // cloned FunctionDef inherited `traitParentCe` from the trait's FunctionDef; + // clear it so the shared `func_decl.h` declaration matches the wrapper's + // own (2-parameter) definition. + $methodDef->functionDef->traitParentCe = false; + // 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 @@ -4390,28 +4407,11 @@ CODE; private function reresolveTraitLateBoundTypes(ClassDef $usingClassDef, MethodDef $methodDef): void { $fn = $methodDef->functionDef; - $needsClone = false; - - if ($fn->returnTypeKeyword !== '') { - $resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); - if ($resolved !== null && $resolved !== $fn->returnClass) { - $needsClone = true; - } - } - foreach ($fn->argInfoList as $arg) { - if ($arg->typeKeyword !== '') { - $resolved = $this->resolveLateBoundClass($usingClassDef, $arg->typeKeyword); - if ($resolved !== null && ($resolved !== $arg->class || $resolved !== $arg->declaredClass)) { - $needsClone = true; - break; - } - } - } - - if (!$needsClone) { - return; - } - + // Always produce a distinct FunctionDef for the composing-class wrapper. + // The wrapper is a separate method (different name, and no implicit + // `trait_parent_ce` parameter) from the trait's own function, so it must + // not share the trait's FunctionDef object — mutating one (e.g. clearing + // `traitParentCe`) would otherwise leak into the trait's declaration. $newFn = clone $fn; if ($fn->returnTypeKeyword !== '') { $resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); From 84aa78a3aacae3488e944dcd80666e368c54b1fb Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 18:40:15 +0800 Subject: [PATCH 2/2] fix(translator): align trait parent wrapper signatures --- .../trait-aliased-constructor-parent-call.php | 5 ++ phpunit/src/TraitFuncDeclTest.php | 12 +++- src/Entity/FunctionDef.php | 10 +-- src/Translator.php | 31 ++++------ ...trait-aliased-constructor-parent-call.phpt | 61 +++++++++++++++++++ 5 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 tests/compiler/trait/trait-aliased-constructor-parent-call.phpt diff --git a/phpunit/code/trait-aliased-constructor-parent-call.php b/phpunit/code/trait-aliased-constructor-parent-call.php index db77562c..42cd4a40 100644 --- a/phpunit/code/trait-aliased-constructor-parent-call.php +++ b/phpunit/code/trait-aliased-constructor-parent-call.php @@ -35,3 +35,8 @@ class Driver extends Base $this->tPdoDriverConstruct($option); } } + +class DirectDriver extends Base +{ + use TPdoDriver; +} diff --git a/phpunit/src/TraitFuncDeclTest.php b/phpunit/src/TraitFuncDeclTest.php index fd06987f..4d5129e6 100644 --- a/phpunit/src/TraitFuncDeclTest.php +++ b/phpunit/src/TraitFuncDeclTest.php @@ -30,10 +30,16 @@ class TraitFuncDeclTest extends \BaseTest $compiler->genFunctionDeclarations($headerPath); $decl = file_get_contents($headerPath); - $this->assertStringContainsString( - 'trait_parent_ce', + $this->assertMatchesRegularExpression( + '/extern void php_tpdodriver____construct\([^;\n]*trait_parent_ce[^;\n]*\);/', $decl, - 'func_decl.h must declare the implicit trait_parent_ce parameter for trait methods with parent:: calls' + 'The trait function declaration must include its implicit parent scope' + ); + $this->assertDoesNotMatchRegularExpression( + '/extern void php_(?:driver__tpdodriverconstruct|directdriver____construct)' + . '\([^;\n]*trait_parent_ce[^;\n]*\);/', + $decl, + 'Composing-class wrapper declarations must not expose the implicit parent scope' ); } } diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 686718b6..1b9f344c 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -33,14 +33,8 @@ class FunctionDef public string $attributeFactoryScope = ''; /** External library imported by the stub containing this function. */ public string $importLibrary = ''; - /** - * True for a trait method whose body contains `parent::` calls. Such methods - * receive an implicit `zend_class_entry *trait_parent_ce` parameter (right after - * `this_`) so the `parent::` call can be bound to the class that composes the - * trait. Both the definition and the shared `func_decl.h` declaration must emit - * this parameter, otherwise the declaration/definition signatures disagree. - */ - public bool $traitParentCe = false; + /** Whether the native signature includes an implicit trait parent scope. */ + public bool $hasTraitParentCeParameter = false; public bool $returnTypeUndeclared = false; public bool $returnsByRef = false; public bool $generator = false; diff --git a/src/Translator.php b/src/Translator.php index d51bf1c0..34e16a90 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1602,10 +1602,7 @@ CODE; $list = []; if ($func->method) { $list[] = Type::OBJECT . ' &this_'; - // A trait method with `parent::` calls receives an implicit - // `trait_parent_ce` parameter right after `this_`. The definition - // adds it (see parseFunction); the declaration must match. - if ($func->traitParentCe) { + if ($func->hasTraitParentCeParameter) { $list[] = 'zend_class_entry *trait_parent_ce'; } } @@ -3465,16 +3462,15 @@ CODE; $cppReturnType = $multiReturn ? $this->functionDef->getMultiReturnCppType() : ($this->functionDef->returnsByRef ? Type::REF : $this->getReturnType()); + $this->functionDef->hasTraitParentCeParameter = + $this->classDef?->trait !== null && (bool) $this->methodDef?->parentMethodCalls; $nativeName = self::PREFIX . $name; $functionAttribute = $this->getFunctionOptimizationAttribute($this->functionDef); $functionDeclCode = $functionAttribute . $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '('; if ($this->class) { $functionDeclCode .= Type::OBJECT . ' &this_'; - if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) { + if ($this->functionDef->hasTraitParentCeParameter) { $functionDeclCode .= ', zend_class_entry *trait_parent_ce'; - // Record the implicit parameter so the shared `func_decl.h` - // declaration (genFunctionDeclaration) emits the same signature. - $this->functionDef->traitParentCe = true; } if ($this->functionDef->params) { $functionDeclCode .= ', '; @@ -4471,6 +4467,10 @@ CODE; string $traitMethodName, string $classMethodName ): string { + // A trait may be composed by multiple classes and aliases. Each wrapper + // needs independent signature metadata. + $methodDef = clone $methodDef; + // A trait method's `self`/`static`/`parent` return and parameter types // refer to the class that uses the trait, not the trait itself. Re-resolve // them to the consuming class so signature-compatibility checks (against @@ -4479,13 +4479,9 @@ CODE; // function untouched. $this->reresolveTraitLateBoundTypes($classDef, $methodDef); - // The wrapper is a method of the *composing* class, not a trait method, so - // it must not receive the implicit `trait_parent_ce` parameter (it computes - // the parent class entry itself when forwarding to the trait function). The - // cloned FunctionDef inherited `traitParentCe` from the trait's FunctionDef; - // clear it so the shared `func_decl.h` declaration matches the wrapper's - // own (2-parameter) definition. - $methodDef->functionDef->traitParentCe = false; + // The wrapper computes the composing class's parent scope and passes it + // to the trait function; it does not expose that scope in its signature. + $methodDef->functionDef->hasTraitParentCeParameter = false; // Validate `parent::` calls emitted from this trait method against the // parent of the class that is composing the trait. The trait itself has @@ -4558,10 +4554,7 @@ CODE; { $fn = $methodDef->functionDef; // Always produce a distinct FunctionDef for the composing-class wrapper. - // The wrapper is a separate method (different name, and no implicit - // `trait_parent_ce` parameter) from the trait's own function, so it must - // not share the trait's FunctionDef object — mutating one (e.g. clearing - // `traitParentCe`) would otherwise leak into the trait's declaration. + // The wrapper has a separate native signature from the trait function. $newFn = clone $fn; if ($fn->returnTypeKeyword !== '') { $resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); diff --git a/tests/compiler/trait/trait-aliased-constructor-parent-call.phpt b/tests/compiler/trait/trait-aliased-constructor-parent-call.phpt new file mode 100644 index 00000000..2547e0ef --- /dev/null +++ b/tests/compiler/trait/trait-aliased-constructor-parent-call.phpt @@ -0,0 +1,61 @@ +--TEST-- +Trait parent constructor calls keep declarations aligned for aliased and direct wrappers +--FILE-- +traitConstruct($options); + } +} + +class DirectDriver extends Base +{ + use DriverConstructor; +} + +function main() +{ + new AliasedDriver(['input' => 1]); + new DirectDriver(['direct' => 1]); +} +?> +--EXPECT-- +array(3) { + ["input"]=> + int(1) + ["alias"]=> + int(1) + ["trait"]=> + int(1) +} +array(2) { + ["direct"]=> + int(1) + ["trait"]=> + int(1) +}