From c4a81466619565eb1f611b9b7aad6c7206e6533a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 30 Jun 2026 10:15:58 +0800 Subject: [PATCH] =?UTF-8?q?refactor(php):=20=E9=87=8D=E6=9E=84PHP=E7=B1=BB?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=92=8C=E6=8E=A5=E5=8F=A3=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加抽象方法签名兼容性检查功能 - 实现接口常量初始化表达式解析支持 - 优化类和接口常量处理流程 - 增强继承关系中抽象方法实现验证 - 改进接口实现方法查找逻辑 - 添加抽象类可延迟实现接口方法的支持 - 修复方法重写签名验证问题 - 重构常量解析为统一方法处理 - 更新类型检查生成器去除排序逻辑 - 扩展测试用例覆盖各种继承场景 --- .../abstract_method_signature_mismatch.php | 15 ++++ .../code/abstract_parent_method_missing.php | 11 +++ .../code/interface_abstract_class_missing.php | 19 +++++ .../interface_abstract_method_mismatch.php | 12 +++ .../interface_abstract_method_signature.php | 20 +++++ .../interface_abstract_parent_missing.php | 15 ++++ phpunit/code/interface_array_constant.php | 7 ++ phpunit/src/InheritanceErrorTest.php | 44 +++++++++++ phpunit/src/PreprocessorTest.php | 14 ++++ src/Php/Entity/ClassDef.php | 5 ++ src/Php/Generator/TypeCheckGenerator.php | 2 - src/Php/Preprocessor.php | 36 +++++---- src/Php/Translator.php | 77 +++++++++++++++++-- 13 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 phpunit/code/abstract_method_signature_mismatch.php create mode 100644 phpunit/code/abstract_parent_method_missing.php create mode 100644 phpunit/code/interface_abstract_class_missing.php create mode 100644 phpunit/code/interface_abstract_method_mismatch.php create mode 100644 phpunit/code/interface_abstract_method_signature.php create mode 100644 phpunit/code/interface_abstract_parent_missing.php create mode 100644 phpunit/code/interface_array_constant.php diff --git a/phpunit/code/abstract_method_signature_mismatch.php b/phpunit/code/abstract_method_signature_mismatch.php new file mode 100644 index 00000000..c23c370e --- /dev/null +++ b/phpunit/code/abstract_method_signature_mismatch.php @@ -0,0 +1,15 @@ +assertCompiles('interface_method_from_trait.php'); } + + public function testAbstractClassMayDeferInterfaceMethodImplementation() + { + $this->assertCompiles('interface_abstract_class_missing.php'); + } + + public function testAbstractMethodMayImplementInterfaceContract() + { + $this->assertCompiles('interface_abstract_method_signature.php'); + } + + public function testAbstractInterfaceMethodSignatureMismatch() + { + $this->exec('must be compatible', 'interface_abstract_method_mismatch.php'); + } + + public function testInterfaceArrayConstantInitializesRuntimeValue() + { + global $translator; + $compiler = CompilerTest::create(ROOT_PATH); + $translator = $compiler; + $testFile = __DIR__ . '/../code/interface_array_constant.php'; + $compiler->addFiles([$testFile]); + $compiler->prepareFile($testFile); + $compiler->convertFile($testFile); + $extensionFile = $compiler->genExtension(); + + $this->assertStringContainsString('php::updateConstant("InterfaceArrayConstant", "ITEMS"', file_get_contents($extensionFile)); + } + + public function testConcreteClassMustImplementInheritedAbstractMethod() + { + $this->exec('must implement abstract method', 'abstract_parent_method_missing.php'); + } + + public function testConcreteClassMustImplementInheritedInterfaceMethod() + { + $this->exec('must implement method', 'interface_abstract_parent_missing.php'); + } + + public function testAbstractMethodSignatureMismatch() + { + $this->exec('must be compatible', 'abstract_method_signature_mismatch.php'); + } } diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php index 19277715..a8162fdc 100644 --- a/phpunit/src/PreprocessorTest.php +++ b/phpunit/src/PreprocessorTest.php @@ -269,6 +269,20 @@ class PreprocessorTest extends TestCase $this->assertNotEmpty($functionDef->returnTypeCheck); } + public function testPrepareFileParsesInterfaceArrayConstantInitExpr(): void + { + $file = __DIR__ . '/../code/interface_array_constant.php'; + + $this->compiler->prepareFile($file); + + $interfaces = $this->getProperty('interfaces'); + $this->assertArrayHasKey('interfacearrayconstant', $interfaces); + $constant = $interfaces['interfacearrayconstant']->constants['ITEMS']; + + $this->assertSame('php::Array', $constant->type); + $this->assertStringContainsString('php::Array', $constant->value); + } + public function testPrepareFileRejectsNamespacedDuplicateClassAndInterface(): void { $this->expectException(TestError::class); diff --git a/src/Php/Entity/ClassDef.php b/src/Php/Entity/ClassDef.php index 0e23e0ee..3282b27a 100644 --- a/src/Php/Entity/ClassDef.php +++ b/src/Php/Entity/ClassDef.php @@ -136,6 +136,11 @@ class ClassDef extends ClassLikeDef return $this->methods[strtolower($method)]; } + public function getAbstractMethod($method): MethodDef + { + return $this->abstractMethodDefs[strtolower($method)]; + } + public function getConstant($name): ConstantDef { return $this->constants[$name]; diff --git a/src/Php/Generator/TypeCheckGenerator.php b/src/Php/Generator/TypeCheckGenerator.php index eb706d49..242b44c0 100644 --- a/src/Php/Generator/TypeCheckGenerator.php +++ b/src/Php/Generator/TypeCheckGenerator.php @@ -130,7 +130,6 @@ trait TypeCheckGenerator foreach ($typeNode->types as $type) { $parts[] = $this->typeCheckNodeToString($type); } - sort($parts); return implode('|', $parts); } if ($typeNode instanceof IntersectionType) { @@ -138,7 +137,6 @@ trait TypeCheckGenerator foreach ($typeNode->types as $type) { $parts[] = $this->typeCheckNodeToString($type); } - sort($parts); return implode('&', $parts); } diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index d2054afb..9d156b6e 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -570,22 +570,30 @@ class Preprocessor extends CompilerBase if ($this->classDef->hasConstant($constName)) { $this->fatalError($v, "Duplicate constant `{$constName}`"); } - $constValue = $this->parseIdentifier($const->value); + $constInfo = $this->parseClassLikeConstant($const, $flags, $type, $class); + $constInfo->class = $class; + $this->classDef->constants[$constInfo->name] = $constInfo; + } + } - $constInfo = new ConstantDef($constName, $flags, $type, $constValue); - $constInfo->valueExpr = $const->value; + private function parseClassLikeConstant(Node\Const_ $const, int $flags, string $type, string $class = ''): ConstantDef + { + $constName = $this->parseIdentifier($const->name); + $constValue = $this->parseIdentifier($const->value); - if ($this->context->beforeStmtLines) { - $arrayExpr = ''; - if ($this->context->localVars) { - $arrayExpr .= $this->genScopeVarDecl(); - } - $arrayExpr .= $this->parseBeforeStmtLines(); - $constInfo->arrayExpr = $arrayExpr; + $constInfo = new ConstantDef($constName, $flags, $type, $constValue); + $constInfo->valueExpr = $const->value; + + if ($this->context->beforeStmtLines) { + $arrayExpr = ''; + if ($this->context->localVars) { + $arrayExpr .= $this->genScopeVarDecl(); } - $constInfo->class = $class; - $this->classDef->constants[$constInfo->name] = $constInfo; + $arrayExpr .= $this->parseBeforeStmtLines(); + $constInfo->arrayExpr = $arrayExpr; } + $constInfo->class = $class; + return $constInfo; } /** @@ -767,9 +775,7 @@ class Preprocessor extends CompilerBase 'Scalar_String' => self::TYPE_STR, default => self::TYPE_VAR, }; - $constInfo = new ConstantDef($constName, $this->parseModifiers($stmt->flags), $type, $this->parseIdentifier($const->value)); - $constInfo->class = $class; - $constInfo->valueExpr = $const->value; + $constInfo = $this->parseClassLikeConstant($const, $this->parseModifiers($stmt->flags), $type, $class); $this->interfaceDef->constants[$constName] = $constInfo; } continue; diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 0a976bc7..8ee92cb6 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -752,7 +752,7 @@ class Translator extends Preprocessor $propCount = max(1, count($this->propMap)); $lines[] = 'extern THREAD_LOCAL uint32_t ' . self::PREFIX . self::PROP_MAP . '[' . $propCount . '];' . PHP_EOL; - foreach ($this->classes as $classDef) { + foreach ($this->getClassLikesWithConstants() as $classDef) { foreach ($classDef->constants as $constant) { if ($constant->type === self::TYPE_ARRAY) { $constName = self::PREFIX . $this->getNativeName($constant->name, $classDef->namespace, $classDef->name); @@ -861,8 +861,8 @@ CODE; } $code .= "// class \n"; - foreach ($this->classes as $classDef) { - if ($classDef->requireCtor) { + foreach ($this->getClassLikesWithConstants() as $classDef) { + if ($classDef instanceof ClassDef && $classDef->requireCtor) { $code .= 'static zend_object* (*create_object_' . $classDef->getNamespacedName() . ")(zend_class_entry *class_type);\n"; } foreach ($classDef->constants as $constant) { @@ -964,7 +964,7 @@ CODE; } $code .= '// class array constants' . PHP_EOL; - foreach ($this->classes as $classDef) { + foreach ($this->getClassLikesWithConstants() as $classDef) { foreach ($classDef->constants as $constant) { if ($constant->type === self::TYPE_ARRAY) { $constName = self::PREFIX . $this->getNativeName($constant->name, $classDef->namespace, $classDef->name); @@ -1885,6 +1885,14 @@ CODE; return self::PREFIX . 'class_entry_' . $this->escapeCeName($classDef->getNamespacedName()); } + /** + * @return array + */ + private function getClassLikesWithConstants(): array + { + return array_merge($this->classes, $this->interfaces); + } + protected function getFilesFromDir(string $path): array { $scanner = new FileScanner($path); @@ -1895,7 +1903,7 @@ CODE; protected function genClassArrayConstants(): string { $code = ''; - foreach ($this->classes as $classDef) { + foreach ($this->getClassLikesWithConstants() as $classDef) { foreach ($classDef->constants as $constant) { if ($constant->type === self::TYPE_ARRAY) { $constName = self::PREFIX . $this->getNativeName($constant->name, $classDef->namespace, $classDef->name); @@ -2739,6 +2747,7 @@ CODE; } if (!$class instanceof Node\Stmt\Trait_) { $this->checkInterfaceImplementations($class); + $this->checkInheritedAbstractMethodsAreImplemented($class); } $code = $this->genNativeMethod($methodCodes); @@ -3092,6 +3101,10 @@ CODE; $this->validateMethodOverrideSignature($v, $name, $this->methodDef, $methodDef, $extends); break; } + if ($classDef->hasAbstractMethod($name) && isset($classDef->abstractMethodDefs[strtolower($name)])) { + $this->validateMethodOverrideSignature($v, $name, $this->methodDef, $classDef->getAbstractMethod($name), $extends); + break; + } } } @@ -3162,11 +3175,31 @@ CODE; private function checkInterfaceImplementations(Node\Stmt\Class_|Node\Stmt\Enum_ $classStmt): void { $classDef = $this->classDef; - foreach ($classDef->implements as $interfaceName) { + foreach ($this->getImplementedInterfacesForClass($classDef) as $interfaceName) { $this->checkInterfaceImplementation($classStmt, $classDef, $interfaceName); } } + /** + * @return array + */ + private function getImplementedInterfacesForClass(ClassDef $classDef): array + { + $interfaces = []; + $current = $classDef; + while (true) { + foreach ($current->implements as $interfaceName) { + $interfaces[$interfaceName] = $interfaceName; + } + if (!$current->extends || !$this->hasClass($current->extends)) { + break; + } + $current = $this->getClass($current->extends); + } + + return array_values($interfaces); + } + private function checkInterfaceImplementation(NodeAbstract $node, ClassDef $classDef, string $interfaceName): void { if ($this->isInternalInterface($interfaceName)) { @@ -3178,8 +3211,11 @@ CODE; $interfaceDef = $this->getInterface($interfaceName); foreach ($interfaceDef->methods as $methodName => $interfaceMethodDef) { - $childMethodDef = $this->findClassMethodDef($classDef, $methodName); + $childMethodDef = $this->findClassMethodDef($classDef, $methodName, $classDef->isAbstract()); if ($childMethodDef === null) { + if ($classDef->isAbstract()) { + continue; + } $this->fatalError($node, "Class `{$classDef->getNamespacedName(false)}` must implement method `{$interfaceName}::{$interfaceMethodDef->name}()`"); } $this->validateMethodOverrideSignature( @@ -3196,13 +3232,16 @@ CODE; } } - private function findClassMethodDef(ClassDef $classDef, string $methodName): ?MethodDef + private function findClassMethodDef(ClassDef $classDef, string $methodName, bool $includeAbstract = true): ?MethodDef { $current = $classDef; while (true) { if ($current->hasMethod($methodName)) { return $current->getMethod($methodName); } + if ($includeAbstract && $current->hasAbstractMethod($methodName) && isset($current->abstractMethodDefs[strtolower($methodName)])) { + return $current->getAbstractMethod($methodName); + } if (!$current->extends || !$this->hasClass($current->extends)) { return null; } @@ -3210,6 +3249,28 @@ CODE; } } + private function checkInheritedAbstractMethodsAreImplemented(NodeAbstract $node): void + { + $classDef = $this->classDef; + if ($classDef->isAbstract()) { + return; + } + + $current = $classDef; + while ($current->extends && $this->hasClass($current->extends)) { + $parent = $this->getClass($current->extends); + foreach ($parent->abstractMethodDefs as $methodName => $abstractMethodDef) { + if ($this->findClassMethodDef($classDef, $methodName, false) === null) { + $this->fatalError( + $node, + "Class `{$classDef->getNamespacedName(false)}` must implement abstract method `{$parent->getNamespacedName(false)}::{$abstractMethodDef->name}()`" + ); + } + } + $current = $parent; + } + } + private function getVisibilityRank(int $flags): int { if ($flags & Modifiers::PUBLIC) {