From 72b80276ba20c5c26bf48a48d48094ff687a6e5c Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 30 Jun 2026 10:29:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(php):=20=E4=BF=AE=E5=A4=8Dtrait=E6=88=90?= =?UTF-8?q?=E5=91=98=E5=86=B2=E7=AA=81=E6=A3=80=E6=B5=8B=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复trait常量冲突检查,只在不兼容时抛出错误 - 修复trait属性冲突检查,只在不兼容时抛出错误 - 添加类型节点转字符串的辅助方法 - 实现兼容性检查方法验证trait常量和属性是否一致 - 更新测试用例验证trait成员兼容性和冲突场景 --- phpunit/code/trait_constant_conflict.php | 14 +++++ phpunit/code/trait_member_compatible.php | 16 ++++++ phpunit/code/trait_property_conflict.php | 14 +++++ phpunit/src/InheritanceErrorTest.php | 15 +++++ src/Php/Translator.php | 70 ++++++++++++++++++++++-- 5 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 phpunit/code/trait_constant_conflict.php create mode 100644 phpunit/code/trait_member_compatible.php create mode 100644 phpunit/code/trait_property_conflict.php diff --git a/phpunit/code/trait_constant_conflict.php b/phpunit/code/trait_constant_conflict.php new file mode 100644 index 00000000..2762737e --- /dev/null +++ b/phpunit/code/trait_constant_conflict.php @@ -0,0 +1,14 @@ +exec('must implement method', 'interface_abstract_parent_missing.php'); } + public function testCompatibleTraitMemberDuplicatesCompile() + { + $this->assertCompiles('trait_member_compatible.php'); + } + + public function testTraitConstantConflict() + { + $this->exec('constant `VALUE` conflicts', 'trait_constant_conflict.php'); + } + + public function testTraitPropertyConflict() + { + $this->exec('property `count` conflicts', 'trait_property_conflict.php'); + } + public function testAbstractMethodSignatureMismatch() { $this->exec('must be compatible', 'abstract_method_signature_mismatch.php'); diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 06ba9390..3ca1c567 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -2585,24 +2585,54 @@ CODE; foreach ($traitStmt->consts as $k2 => $const) { $constName = strtolower($const->name->toString()); if (isset($constants[$constName])) { - unset($traitStmts[$k1][$k2]); + unset($traitStmt->consts[$k2]); + if (!$traitStmt->consts) { + unset($traitStmts[$k1]); + } + continue; } if (isset($traitConstants[$constName])) { - $this->fatalError($classStmt, "Trait `{$traitFullName}` constant `{$constName}` already exists"); + [$existingConstStmt, $existingConst] = $traitConstants[$constName]; + if ($existingConstStmt->flags !== $traitStmt->flags || + $this->typeNodeToStringOrNull($existingConstStmt->type) !== $this->typeNodeToStringOrNull($traitStmt->type) || + $this->printer->prettyPrintExpr($existingConst->value) !== $this->printer->prettyPrintExpr($const->value)) { + $this->fatalError($classStmt, "Trait `{$traitFullName}` constant `{$constName}` already exists"); + } + unset($traitStmt->consts[$k2]); + if (!$traitStmt->consts) { + unset($traitStmts[$k1]); + } + continue; } - $traitConstants[$constName] = $const; + $traitConstants[$constName] = [$traitStmt, $const]; } } if ($traitStmt instanceof Node\Stmt\Property) { foreach ($traitStmt->props as $k2 => $prop) { $propName = strtolower($prop->name->toString()); if (isset($properties[$propName])) { - unset($traitStmts[$k1][$k2]); + unset($traitStmt->props[$k2]); + if (!$traitStmt->props) { + unset($traitStmts[$k1]); + } + continue; } if (isset($traitProperties[$propName])) { - $this->fatalError($classStmt, "Trait `{$traitFullName}` property `{$propName}` already exists"); + [$existingPropStmt, $existingProp] = $traitProperties[$propName]; + $existingDefault = $existingProp->default ? $this->printer->prettyPrintExpr($existingProp->default) : null; + $propDefault = $prop->default ? $this->printer->prettyPrintExpr($prop->default) : null; + if ($existingPropStmt->flags !== $traitStmt->flags || + $this->typeNodeToStringOrNull($existingPropStmt->type) !== $this->typeNodeToStringOrNull($traitStmt->type) || + $existingDefault !== $propDefault) { + $this->fatalError($classStmt, "Trait `{$traitFullName}` property `{$propName}` already exists"); + } + unset($traitStmt->props[$k2]); + if (!$traitStmt->props) { + unset($traitStmts[$k1]); + } + continue; } - $traitProperties[$propName] = $prop; + $traitProperties[$propName] = [$traitStmt, $prop]; } } } @@ -2717,6 +2747,11 @@ CODE; return $this->printer->prettyPrint([$typeNode]); } + private function typeNodeToStringOrNull(?NodeAbstract $typeNode): ?string + { + return $typeNode ? $this->typeNodeToString($typeNode) : null; + } + protected function parseClass(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): string { $this->class = $this->parseIdentifier($class->name); @@ -3389,12 +3424,18 @@ CODE; // 将 Trait 中定义的 常量、静态常量、属性、方法、静态属性复制到当前类中 foreach ($traitDef->constants as $const) { if ($classDef->hasConstant($const->name)) { + if (!$this->isCompatibleTraitConstant($classDef->getConstant($const->name), $const)) { + $this->fatalError($v, "Trait `{$traitFullName}` constant `{$const->name}` conflicts with class `{$classDef->getNamespacedName(false)}`"); + } continue; } $classDef->constants[$const->name] = $const; } foreach ($traitDef->properties as $prop) { if ($classDef->hasProperty($prop->name)) { + if (!$this->isCompatibleTraitProperty($classDef->getProperty($prop->name), $prop)) { + $this->fatalError($v, "Trait `{$traitFullName}` property `{$prop->name}` conflicts with class `{$classDef->getNamespacedName(false)}`"); + } continue; } $classDef->properties[$prop->name] = $prop; @@ -3454,6 +3495,23 @@ CODE; } } + private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool + { + return $existing->flags === $incoming->flags && + $existing->type === $incoming->type && + $existing->class === $incoming->class && + $existing->value === $incoming->value; + } + + private function isCompatibleTraitProperty(PropertyDef $existing, PropertyDef $incoming): bool + { + return $existing->flags === $incoming->flags && + $existing->type === $incoming->type && + $existing->class === $incoming->class && + $existing->nullable === $incoming->nullable && + $existing->default === $incoming->default; + } + protected function parseForeachObject(Foreach_ $node): string { $obj = $this->parseIdentifier($node->expr);