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..42cd4a40 --- /dev/null +++ b/phpunit/code/trait-aliased-constructor-parent-call.php @@ -0,0 +1,42 @@ + $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); + } +} + +class DirectDriver extends Base +{ + use TPdoDriver; +} diff --git a/phpunit/src/TraitFuncDeclTest.php b/phpunit/src/TraitFuncDeclTest.php new file mode 100644 index 00000000..4d5129e6 --- /dev/null +++ b/phpunit/src/TraitFuncDeclTest.php @@ -0,0 +1,45 @@ +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->assertMatchesRegularExpression( + '/extern void php_tpdodriver____construct\([^;\n]*trait_parent_ce[^;\n]*\);/', + $decl, + '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 028b69f9..1b9f344c 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -33,6 +33,8 @@ class FunctionDef public string $attributeFactoryScope = ''; /** External library imported by the stub containing this function. */ public string $importLibrary = ''; + /** 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 66568139..34e16a90 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1602,6 +1602,9 @@ CODE; $list = []; if ($func->method) { $list[] = Type::OBJECT . ' &this_'; + if ($func->hasTraitParentCeParameter) { + $list[] = 'zend_class_entry *trait_parent_ce'; + } } $argInfoList = $func->argInfoList; if ($argInfoList) { @@ -3459,12 +3462,14 @@ 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'; } if ($this->functionDef->params) { @@ -4462,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 @@ -4470,6 +4479,10 @@ CODE; // function untouched. $this->reresolveTraitLateBoundTypes($classDef, $methodDef); + // 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 // no parent at compile time, so this is the only place the parent class @@ -4540,28 +4553,8 @@ 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 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) +}