feat(trait): 支持trait方法多别名和insteadof语法

- 实现trait方法可以有多个别名的功能
- 支持insteadof语法在trait列表中的顺序无关性
- 保持trait别名的原始方法功能
- 添加对继承中静态方法不匹配的检查
- 更新trait别名数据结构以支持别名列表
- 重构trait方法包装器为独立函数
- 修复trait方法别名处理逻辑错误
pull/5/head
韩天峰 2 months ago
parent 72b80276ba
commit 09be0226cb
  1. 15
      phpunit/code/inheritance_error_static.php
  2. 13
      phpunit/code/interface_method_static_mismatch.php
  3. 10
      phpunit/src/InheritanceErrorTest.php
  4. 4
      phpunit/src/PreprocessorTest.php
  5. 4
      src/Php/Entity/ClassDef.php
  6. 8
      src/Php/Preprocessor.php
  7. 158
      src/Php/Translator.php
  8. 29
      tests/aot/trait/015.phpt
  9. 38
      tests/aot/trait/016.phpt
  10. 32
      tests/aot/trait/017.phpt

@ -0,0 +1,15 @@
<?php
class InheritanceStaticParent
{
public static function run(): void
{
}
}
class InheritanceStaticChild extends InheritanceStaticParent
{
public function run(): void
{
}
}

@ -0,0 +1,13 @@
<?php
interface InterfaceStaticContract
{
public static function run(): void;
}
class InterfaceStaticImpl implements InterfaceStaticContract
{
public function run(): void
{
}
}

@ -68,6 +68,16 @@ class InheritanceErrorTest extends TestCase
$this->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');

@ -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

@ -57,8 +57,8 @@ class ClassDef extends ClassLikeDef
public ?Trait_ $trait = null;
/**
* FullMethodName -> NewMethodName
* @var array<string, array>
* FullMethodName -> alias list
* @var array<string, array<int, array{newName: string, newModifier: int}>>
*/
public array $traitAliases = [];

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

@ -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

@ -0,0 +1,29 @@
--TEST--
Trait alias keeps original method
--FILE--
<?php
trait TraitAliasOriginal
{
public function hello(): void
{
echo "hello\n";
}
}
class TraitAliasOriginalUser
{
use TraitAliasOriginal {
hello as hi;
}
}
function main()
{
$user = new TraitAliasOriginalUser();
$user->hello();
$user->hi();
}
?>
--EXPECT--
hello
hello

@ -0,0 +1,38 @@
--TEST--
Trait insteadof works regardless of trait list order
--FILE--
<?php
trait TraitOrderA
{
public function hello(): void
{
echo "A\n";
}
}
trait TraitOrderB
{
public function hello(): void
{
echo "B\n";
}
}
class TraitOrderUser
{
use TraitOrderB, TraitOrderA {
TraitOrderB::hello insteadof TraitOrderA;
TraitOrderA::hello as helloA;
}
}
function main()
{
$user = new TraitOrderUser();
$user->hello();
$user->helloA();
}
?>
--EXPECT--
B
A

@ -0,0 +1,32 @@
--TEST--
Trait method can have multiple aliases
--FILE--
<?php
trait TraitMultipleAlias
{
public function hello(): void
{
echo "hello\n";
}
}
class TraitMultipleAliasUser
{
use TraitMultipleAlias {
hello as hi;
hello as hey;
}
}
function main()
{
$user = new TraitMultipleAliasUser();
$user->hello();
$user->hi();
$user->hey();
}
?>
--EXPECT--
hello
hello
hello
Loading…
Cancel
Save