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) +}