From 09be0226cb37ee66e85e4dbb24888f8a44261253 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 30 Jun 2026 10:47:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(trait):=20=E6=94=AF=E6=8C=81trait=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=A4=9A=E5=88=AB=E5=90=8D=E5=92=8Cinsteadof=E8=AF=AD?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现trait方法可以有多个别名的功能 - 支持insteadof语法在trait列表中的顺序无关性 - 保持trait别名的原始方法功能 - 添加对继承中静态方法不匹配的检查 - 更新trait别名数据结构以支持别名列表 - 重构trait方法包装器为独立函数 - 修复trait方法别名处理逻辑错误 --- phpunit/code/inheritance_error_static.php | 15 ++ .../code/interface_method_static_mismatch.php | 13 ++ phpunit/src/InheritanceErrorTest.php | 10 ++ phpunit/src/PreprocessorTest.php | 4 +- src/Php/Entity/ClassDef.php | 4 +- src/Php/Preprocessor.php | 8 +- src/Php/Translator.php | 158 ++++++++++++------ tests/aot/trait/015.phpt | 29 ++++ tests/aot/trait/016.phpt | 38 +++++ tests/aot/trait/017.phpt | 32 ++++ 10 files changed, 257 insertions(+), 54 deletions(-) create mode 100644 phpunit/code/inheritance_error_static.php create mode 100644 phpunit/code/interface_method_static_mismatch.php create mode 100644 tests/aot/trait/015.phpt create mode 100644 tests/aot/trait/016.phpt create mode 100644 tests/aot/trait/017.phpt diff --git a/phpunit/code/inheritance_error_static.php b/phpunit/code/inheritance_error_static.php new file mode 100644 index 00000000..655844c8 --- /dev/null +++ b/phpunit/code/inheritance_error_static.php @@ -0,0 +1,15 @@ +assertCompiles('inheritance_error_visibility.php'); } + public function testMethodStaticMismatch() + { + $this->exec('must be compatible', 'inheritance_error_static.php'); + } + + public function testInterfaceMethodStaticMismatch() + { + $this->exec('must be compatible', 'interface_method_static_mismatch.php'); + } + public function testChildMayAddOptionalTrailingParameter() { $this->assertCompiles('inheritance_optional_param_allowed.php'); diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php index a8162fdc..39888c4d 100644 --- a/phpunit/src/PreprocessorTest.php +++ b/phpunit/src/PreprocessorTest.php @@ -332,8 +332,8 @@ class PreprocessorTest extends TestCase $this->assertArrayHasKey('aliasmodifieruser', $classes); $aliases = $classes['aliasmodifieruser']->traitAliases; $this->assertArrayHasKey('aliasmodifiertrait::hello', $aliases); - $this->assertSame('hello', $aliases['aliasmodifiertrait::hello']['newName']); - $this->assertSame(Modifiers::PRIVATE, $aliases['aliasmodifiertrait::hello']['newModifier']); + $this->assertSame('hello', $aliases['aliasmodifiertrait::hello'][0]['newName']); + $this->assertSame(Modifiers::PRIVATE, $aliases['aliasmodifiertrait::hello'][0]['newModifier']); } public function testPrepareFileInfersEachClassConstantTypeIndependently(): void diff --git a/src/Php/Entity/ClassDef.php b/src/Php/Entity/ClassDef.php index 3282b27a..ae3703c5 100644 --- a/src/Php/Entity/ClassDef.php +++ b/src/Php/Entity/ClassDef.php @@ -57,8 +57,8 @@ class ClassDef extends ClassLikeDef public ?Trait_ $trait = null; /** - * FullMethodName -> NewMethodName - * @var array + * FullMethodName -> alias list + * @var array> */ public array $traitAliases = []; diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index 9d156b6e..feff1478 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -829,7 +829,7 @@ class Preprocessor extends CompilerBase * use TraitA { TraitA::method as newMethod} * 这表示 TraitA::method() 会被重命名为 TraitA::newMethod() */ - $aliases[$this->getFullMethodName($traitName, $methodName)] = [ + $aliases[$this->getFullMethodName($traitName, $methodName)][] = [ 'newName' => $adaptation->newName ? $adaptation->newName->toString() : $methodName, 'newModifier' => $adaptation->newModifier ?: 0, ]; @@ -866,7 +866,11 @@ class Preprocessor extends CompilerBase $this->symbolCallInFile[$this->file][] = strtolower($traitName); } } - $this->classDef->traitAliases = array_merge($this->classDef->traitAliases, $aliases); + foreach ($aliases as $fullMethodName => $aliasList) { + foreach ($aliasList as $alias) { + $this->classDef->traitAliases[$fullMethodName][] = $alias; + } + } $this->classDef->traitIgnored = array_merge($this->classDef->traitIgnored, $ignored); } } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 3ca1c567..7a69a90e 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -2528,9 +2528,31 @@ CODE; $traitAst = clone $traitDef->trait; $traitStmts = $traitAst->stmts; + $aliasStmts = []; foreach ($traitStmts as $k1 => $traitStmt) { if ($traitStmt instanceof Node\Stmt\ClassMethod) { $methodName = strtolower($traitStmt->name->toString()); + $fullMethodName = $this->getFullMethodName($traitFullName, $methodName); + foreach ($classDef->traitAliases[$fullMethodName] ?? [] as $alias) { + $aliasName = strtolower($alias['newName']); + if ($aliasName === $methodName) { + if ($alias['newModifier']) { + $traitStmt->flags = $alias['newModifier']; + } + } elseif (!isset($methods[$aliasName]) && !isset($traitMethods[$aliasName])) { + $aliasStmt = clone $traitStmt; + $aliasStmt->name = new Node\Identifier($alias['newName']); + if ($alias['newModifier']) { + $aliasStmt->flags = $alias['newModifier']; + } + $aliasStmts[] = $aliasStmt; + $traitMethods[$aliasName] = [$traitFullName, $aliasStmt]; + } + } + if (isset($classDef->traitIgnored[$fullMethodName])) { + unset($traitStmts[$k1]); + continue; + } if (isset($traitMethods[$methodName])) { [$existingTraitName, $existingStmt] = $traitMethods[$methodName]; $newAbstract = $traitStmt->isAbstract(); @@ -2563,19 +2585,6 @@ CODE; // Both concrete — error $this->fatalError($classStmt, "Trait `{$traitFullName}` method `{$methodName}` already exists"); } - $fullMethodName = $this->getFullMethodName($traitFullName, $methodName); - if (isset($classDef->traitAliases[$fullMethodName])) { - $alias = $classDef->traitAliases[$fullMethodName]; - $methodName = $alias['newName']; - $traitStmt->name = new Node\Identifier($methodName); - if ($alias['newModifier']) { - $traitStmt->flags = $alias['newModifier']; - } - } - if (isset($classDef->traitIgnored[$fullMethodName])) { - unset($traitStmts[$k1]); - continue; - } if (isset($methods[$methodName])) { unset($traitStmts[$k1]); } @@ -2637,7 +2646,7 @@ CODE; } } - $stmt->stmts = array_merge($stmt->stmts, $traitStmts); + $stmt->stmts = array_merge($stmt->stmts, $traitStmts, $aliasStmts); } } } @@ -3194,6 +3203,10 @@ CODE; $error('visibility mismatch'); } + if (($childMethodDef->flags & Modifiers::STATIC) !== ($parentMethodDef->flags & Modifiers::STATIC)) { + $error('static mismatch'); + } + $childFuncDef = $childMethodDef->functionDef; $parentFuncDef = $parentMethodDef->functionDef; if (!$childFuncDef || !$parentFuncDef) { @@ -3443,56 +3456,105 @@ CODE; foreach ($traitDef->methods as $methodDef) { $classMethodName = $traitMethodName = $methodDef->name; $fullMethodName = $this->getFullMethodName($traitFullName, $traitMethodName); - // Trait 设置了别名 - if (isset($classDef->traitAliases[$fullMethodName])) { - $alias = $classDef->traitAliases[$fullMethodName]; - $methodDef = clone $methodDef; - $classMethodName = $methodDef->name = $alias['newName']; + $originalMethodDef = $methodDef; + $aliasMethodDefs = []; + foreach ($classDef->traitAliases[$fullMethodName] ?? [] as $alias) { + if (strtolower($alias['newName']) === strtolower($traitMethodName)) { + if ($alias['newModifier']) { + if ($originalMethodDef === $methodDef) { + $originalMethodDef = clone $methodDef; + } + $originalMethodDef->flags = $this->parseModifiers($alias['newModifier']); + } + continue; + } + $aliasMethodDef = clone $methodDef; + $classMethodName = $aliasMethodDef->name = $alias['newName']; if ($alias['newModifier']) { - $methodDef->flags = $this->parseModifiers($alias['newModifier']); + $aliasMethodDef->flags = $this->parseModifiers($alias['newModifier']); } + $aliasMethodDefs[strtolower($classMethodName)] = [$classMethodName, $aliasMethodDef]; } // 设置了 insteadof 选项,此 Trait 的方法将不会被使用 if (isset($classDef->traitIgnored[$fullMethodName])) { + foreach ($aliasMethodDefs as [$aliasMethodName, $aliasMethodDef]) { + if ($classDef->hasMethod($aliasMethodName)) { + continue; + } + $methodCodes[$aliasMethodName] = $this->addTraitMethodWrapper( + $classDef, + $traitDef, + $aliasMethodDef, + $traitMethodName, + $aliasMethodName + ); + } continue; } // 类中已经有同名方法,则不使用 Trait 中的方法 - if ($classDef->hasMethod($classMethodName)) { - continue; - } - - $classDef->addMethod($methodDef); - $traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name); - $classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name); - $argList = ['this_']; - foreach ($methodDef->functionDef->argInfoList as $argInfo) { - $argList[] = $argInfo->name; + if (!$classDef->hasMethod($traitMethodName)) { + $methodCodes[$traitMethodName] = $this->addTraitMethodWrapper( + $classDef, + $traitDef, + $originalMethodDef, + $traitMethodName, + $traitMethodName + ); } - $argv = implode(', ', $argList); - $code = $methodDef->getReturnType() . ' ' . self::PREFIX . $classMethodNativeName . '('; - if ($this->class) { - $code .= self::TYPE_OBJECT . ' &this_'; - if ($methodDef->functionDef->params) { - $code .= ', '; + foreach ($aliasMethodDefs as [$aliasMethodName, $aliasMethodDef]) { + if ($classDef->hasMethod($aliasMethodName)) { + continue; } + $methodCodes[$aliasMethodName] = $this->addTraitMethodWrapper( + $classDef, + $traitDef, + $aliasMethodDef, + $traitMethodName, + $aliasMethodName + ); } + } + } + } - $this->addFunction($classMethodNativeName, $methodDef->functionDef); + private function addTraitMethodWrapper( + ClassDef $classDef, + ClassDef $traitDef, + MethodDef $methodDef, + string $traitMethodName, + string $classMethodName + ): string { + $classDef->addMethod($methodDef); + $traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name); + $classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name); + $argList = ['this_']; + foreach ($methodDef->functionDef->argInfoList as $argInfo) { + $argList[] = $argInfo->name; + } + $argv = implode(', ', $argList); - $code .= $methodDef->functionDef->params . ')'; - $code .= '{' . PHP_EOL; - $this->indentLevel++; - $methodCall = self::PREFIX . $traitMethodNativeName . '(' . $argv . ')'; - if ($methodDef->getReturnType() !== self::TYPE_VOID) { - $methodCall = 'return ' . $methodCall; - } - $code .= $this->getIndent() . $methodCall . ';' . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '}' . PHP_EOL; - $methodCodes[$classMethodName] = $code; + $code = $methodDef->getReturnType() . ' ' . self::PREFIX . $classMethodNativeName . '('; + if ($this->class) { + $code .= self::TYPE_OBJECT . ' &this_'; + if ($methodDef->functionDef->params) { + $code .= ', '; } } + + $this->addFunction($classMethodNativeName, $methodDef->functionDef); + + $code .= $methodDef->functionDef->params . ')'; + $code .= '{' . PHP_EOL; + $this->indentLevel++; + $methodCall = self::PREFIX . $traitMethodNativeName . '(' . $argv . ')'; + if ($methodDef->getReturnType() !== self::TYPE_VOID) { + $methodCall = 'return ' . $methodCall; + } + $code .= $this->getIndent() . $methodCall . ';' . PHP_EOL; + $this->indentLevel--; + $code .= $this->getIndent() . '}' . PHP_EOL; + return $code; } private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool diff --git a/tests/aot/trait/015.phpt b/tests/aot/trait/015.phpt new file mode 100644 index 00000000..d0475d0d --- /dev/null +++ b/tests/aot/trait/015.phpt @@ -0,0 +1,29 @@ +--TEST-- +Trait alias keeps original method +--FILE-- +hello(); + $user->hi(); +} +?> +--EXPECT-- +hello +hello diff --git a/tests/aot/trait/016.phpt b/tests/aot/trait/016.phpt new file mode 100644 index 00000000..303afdb3 --- /dev/null +++ b/tests/aot/trait/016.phpt @@ -0,0 +1,38 @@ +--TEST-- +Trait insteadof works regardless of trait list order +--FILE-- +hello(); + $user->helloA(); +} +?> +--EXPECT-- +B +A diff --git a/tests/aot/trait/017.phpt b/tests/aot/trait/017.phpt new file mode 100644 index 00000000..65f370ca --- /dev/null +++ b/tests/aot/trait/017.phpt @@ -0,0 +1,32 @@ +--TEST-- +Trait method can have multiple aliases +--FILE-- +hello(); + $user->hi(); + $user->hey(); +} +?> +--EXPECT-- +hello +hello +hello