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);