From 3ca0d64a6a6c11380dd245ee31fa8bce70b0784a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 30 Jun 2026 09:57:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(preprocessor):=20=E5=A2=9E=E5=BC=BAPHP?= =?UTF-8?q?=E9=A2=84=E5=A4=84=E7=90=86=E5=99=A8=E5=8A=9F=E8=83=BD=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=8E=A5=E5=8F=A3=E6=88=90=E5=91=98=E5=92=8C=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现接口方法、常量和继承列表的解析与存储 - 添加对抽象方法签名的完整解析支持 - 增加命名空间常量的解析和类型推断功能 - 实现接口实现验证和方法签名匹配检查 - 支持多重接口继承关系的解析处理 - 添加重复定义(类、接口、常量、方法)的检测机制 - 实现trait别名和修饰符的正确解析 - 增强依赖排序算法支持接口和trait依赖关系 - 完善方法重写签名验证逻辑 - 添加接口方法由trait提供的情况处理 --- phpunit/code/compiler_api/hello_world.cc | 1 + phpunit/code/compiler_api/ignored.php | 2 + phpunit/code/compiler_api/kept.php | 2 + phpunit/code/compiler_api/main.php | 2 + phpunit/code/compiler_api/skipped_nested.php | 2 + phpunit/code/interface_method_from_trait.php | 20 +++ phpunit/code/interface_method_missing.php | 11 ++ .../interface_method_signature_mismatch.php | 15 ++ .../abstract_method_signature.php | 4 + phpunit/code/preprocessor/class_constants.php | 4 + .../preprocessor/deps_class_implements.php | 2 + .../preprocessor/deps_class_uses_trait.php | 4 + phpunit/code/preprocessor/deps_interface.php | 2 + phpunit/code/preprocessor/deps_trait.php | 2 + .../duplicate_abstract_method.php | 5 + .../preprocessor/duplicate_class_constant.php | 5 + .../duplicate_namespaced_class_interface.php | 4 + .../code/preprocessor/interface_members.php | 7 + .../code/preprocessor/namespaced_const.php | 3 + .../preprocessor/trait_alias_modifier.php | 10 ++ phpunit/src/CompilerBaseApiTest.php | 15 +- phpunit/src/Entity/InterfaceDefTest.php | 19 +++ phpunit/src/InheritanceErrorTest.php | 15 ++ phpunit/src/PreprocessorTest.php | 136 ++++++++++++++++++ src/Php/CompilerBase.php | 26 +++- src/Php/Entity/ClassDef.php | 14 +- src/Php/Entity/InterfaceDef.php | 30 ++++ src/Php/Preprocessor.php | 104 ++++++++++++-- src/Php/Translator.php | 84 +++++++++-- .../{ns-const.phpt => ns-const-01.phpt} | 0 tests/aot/namespace/ns-const-02.phpt | 28 ++++ tests/aot/namespace/ns-const-03.phpt | 24 ++++ 32 files changed, 566 insertions(+), 36 deletions(-) create mode 100644 phpunit/code/compiler_api/hello_world.cc create mode 100644 phpunit/code/compiler_api/ignored.php create mode 100644 phpunit/code/compiler_api/kept.php create mode 100644 phpunit/code/compiler_api/main.php create mode 100644 phpunit/code/compiler_api/skipped_nested.php create mode 100644 phpunit/code/interface_method_from_trait.php create mode 100644 phpunit/code/interface_method_missing.php create mode 100644 phpunit/code/interface_method_signature_mismatch.php create mode 100644 phpunit/code/preprocessor/abstract_method_signature.php create mode 100644 phpunit/code/preprocessor/class_constants.php create mode 100644 phpunit/code/preprocessor/deps_class_implements.php create mode 100644 phpunit/code/preprocessor/deps_class_uses_trait.php create mode 100644 phpunit/code/preprocessor/deps_interface.php create mode 100644 phpunit/code/preprocessor/deps_trait.php create mode 100644 phpunit/code/preprocessor/duplicate_abstract_method.php create mode 100644 phpunit/code/preprocessor/duplicate_class_constant.php create mode 100644 phpunit/code/preprocessor/duplicate_namespaced_class_interface.php create mode 100644 phpunit/code/preprocessor/interface_members.php create mode 100644 phpunit/code/preprocessor/namespaced_const.php create mode 100644 phpunit/code/preprocessor/trait_alias_modifier.php rename tests/aot/namespace/{ns-const.phpt => ns-const-01.phpt} (100%) create mode 100644 tests/aot/namespace/ns-const-02.phpt create mode 100644 tests/aot/namespace/ns-const-03.phpt diff --git a/phpunit/code/compiler_api/hello_world.cc b/phpunit/code/compiler_api/hello_world.cc new file mode 100644 index 00000000..76e81970 --- /dev/null +++ b/phpunit/code/compiler_api/hello_world.cc @@ -0,0 +1 @@ +int main() { return 0; } diff --git a/phpunit/code/compiler_api/ignored.php b/phpunit/code/compiler_api/ignored.php new file mode 100644 index 00000000..f95a9e1b --- /dev/null +++ b/phpunit/code/compiler_api/ignored.php @@ -0,0 +1,2 @@ +invoke($this->compiler, ...$args); } + private function fixturePath(string $file): string + { + return __DIR__ . '/../code/compiler_api/' . $file; + } + private function createProjectFile(string $yaml, string $filename = 'project.yml', string $baseDir = ''): string { $projectDir = $baseDir === '' ? $this->testDir : $this->testDir . '/' . trim($baseDir, '/'); @@ -82,7 +87,7 @@ class CompilerBaseApiTest extends TestCase } $sourceFile = $projectDir . '/main.php'; - file_put_contents($sourceFile, "fixturePath('main.php'), $sourceFile); $projectFile = $projectDir . '/' . $filename; file_put_contents($projectFile, $yaml); @@ -363,9 +368,9 @@ ignore: YAML); $projectDir = dirname($projectFile); mkdir($projectDir . '/skipped', 0777, true); - file_put_contents($projectDir . '/ignored.php', "fixturePath('ignored.php'), $projectDir . '/ignored.php'); + copy($this->fixturePath('skipped_nested.php'), $projectDir . '/skipped/nested.php'); + copy($this->fixturePath('kept.php'), $projectDir . '/kept.php'); $files = $this->invokeMethod('parseProjectYaml', $projectFile); @@ -452,7 +457,7 @@ YAML); $logFile = $spaceDir . '/format.log'; $sourceFile = $spaceDir . '/hello world.cc'; - file_put_contents($sourceFile, "int main() { return 0; }\n"); + copy($this->fixturePath('hello_world.cc'), $sourceFile); $this->createFakeClangFormat($binDir, $logFile); putenv('PATH=' . $binDir . ':' . ($this->originalPath ?: '')); diff --git a/phpunit/src/Entity/InterfaceDefTest.php b/phpunit/src/Entity/InterfaceDefTest.php index 645847ff..b769aacc 100644 --- a/phpunit/src/Entity/InterfaceDefTest.php +++ b/phpunit/src/Entity/InterfaceDefTest.php @@ -4,6 +4,7 @@ namespace PhpAot\Tests\Entity; use PHPUnit\Framework\TestCase; use PhpAot\Php\Entity\InterfaceDef; +use PhpAot\Php\Entity\MethodDef; class InterfaceDefTest extends TestCase { @@ -39,4 +40,22 @@ class InterfaceDefTest extends TestCase $this->assertEquals('Parent', $iface->extends); } + + public function testTracksMethodsCaseInsensitively(): void + { + $iface = new InterfaceDef('Runnable'); + $iface->addMethod(new MethodDef(0, 'run')); + + $this->assertTrue($iface->hasMethod('run')); + $this->assertTrue($iface->hasMethod('RUN')); + $this->assertArrayHasKey('run', $iface->methods); + } + + public function testTracksMultipleParentInterfaces(): void + { + $iface = new InterfaceDef('Child'); + $iface->extendsList = ['ParentA', 'ParentB']; + + $this->assertSame(['ParentA', 'ParentB'], $iface->extendsList); + } } diff --git a/phpunit/src/InheritanceErrorTest.php b/phpunit/src/InheritanceErrorTest.php index 6453e860..b1f0e0cd 100644 --- a/phpunit/src/InheritanceErrorTest.php +++ b/phpunit/src/InheritanceErrorTest.php @@ -97,4 +97,19 @@ class InheritanceErrorTest extends TestCase { $this->exec('must be compatible', 'inheritance_error_prop_readonly.php'); } + + public function testInterfaceMethodMissing() + { + $this->exec('must implement method', 'interface_method_missing.php'); + } + + public function testInterfaceMethodSignatureMismatch() + { + $this->exec('must be compatible', 'interface_method_signature_mismatch.php'); + } + + public function testInterfaceMethodProvidedByTrait() + { + $this->assertCompiles('interface_method_from_trait.php'); + } } diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php index 3e1f1bc2..19277715 100644 --- a/phpunit/src/PreprocessorTest.php +++ b/phpunit/src/PreprocessorTest.php @@ -5,8 +5,10 @@ namespace PhpAot\Tests; use PHPUnit\Framework\TestCase; use PhpAot\Php\CompilerTest; use PhpAot\Php\ArgInfo; +use PhpAot\Php\Exception\TestError; use PhpParser\Node; use PhpParser\Node\Stmt\Function_; +use PhpParser\Modifiers; use PhpParser\ParserFactory; class PreprocessorTest extends TestCase @@ -59,6 +61,13 @@ class PreprocessorTest extends TestCase $prop->setValue($this->compiler, $value); } + private function getProperty(string $name): mixed + { + $prop = $this->ref->getProperty($name); + $prop->setAccessible(true); + return $prop->getValue($this->compiler); + } + private function parseFunctionNode(string $code): Function_ { $parser = (new ParserFactory())->createForHostVersion(); @@ -235,6 +244,133 @@ class PreprocessorTest extends TestCase $this->assertIsArray($files); } + public function testPrepareFileParsesInterfaceMembersAndTypeChecks(): void + { + $file = __DIR__ . '/../code/preprocessor/interface_members.php'; + + $this->compiler->prepareFile($file); + + $interfaces = $this->getProperty('interfaces'); + $this->assertArrayHasKey('demo', $interfaces); + + $iface = $interfaces['demo']; + $this->assertSame(['ParentA', 'ParentB'], $iface->extendsList); + $this->assertSame('ParentA', $iface->extends); + $this->assertArrayHasKey('VERSION', $iface->constants); + $this->assertTrue($iface->hasMethod('run')); + + $functionDef = $iface->methods['run']->functionDef; + $this->assertTrue($functionDef->method); + $this->assertSame('php::Int', $functionDef->argInfoList[0]->type); + $this->assertNull($functionDef->argInfoList[0]->typeCheck); + $this->assertSame('php::Var', $functionDef->argInfoList[1]->type); + $this->assertNotEmpty($functionDef->argInfoList[1]->typeCheck); + $this->assertSame('php::Var', $functionDef->returnType); + $this->assertNotEmpty($functionDef->returnTypeCheck); + } + + public function testPrepareFileRejectsNamespacedDuplicateClassAndInterface(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Duplicate class `App\\Demo`'); + + $file = __DIR__ . '/../code/preprocessor/duplicate_namespaced_class_interface.php'; + $this->compiler->prepareFile($file); + } + + public function testPrepareFileParsesNamespacedConstants(): void + { + $file = __DIR__ . '/../code/preprocessor/namespaced_const.php'; + + $this->compiler->prepareFile($file); + + $constants = $this->getProperty('constants'); + $this->assertArrayHasKey('_const_var_App__VERSION', $constants); + $this->assertSame('App\\VERSION', $constants['_const_var_App__VERSION']->name); + } + + public function testSortFilesUsesImplementsAndTraitDependencies(): void + { + $classFile = realpath(__DIR__ . '/../code/preprocessor/deps_class_implements.php'); + $interfaceFile = realpath(__DIR__ . '/../code/preprocessor/deps_interface.php'); + $traitUserFile = realpath(__DIR__ . '/../code/preprocessor/deps_class_uses_trait.php'); + $traitFile = realpath(__DIR__ . '/../code/preprocessor/deps_trait.php'); + + $this->compiler->prepareFile($classFile); + $this->compiler->prepareFile($interfaceFile); + $this->compiler->prepareFile($traitUserFile); + $this->compiler->prepareFile($traitFile); + + $files = [$classFile, $interfaceFile, $traitUserFile, $traitFile]; + $this->compiler->sortFiles($files); + + $this->assertLessThan(array_search($classFile, $files, true), array_search($interfaceFile, $files, true)); + $this->assertLessThan(array_search($traitUserFile, $files, true), array_search($traitFile, $files, true)); + } + + public function testPrepareFileParsesTraitAliasModifierWithoutNewName(): void + { + $file = __DIR__ . '/../code/preprocessor/trait_alias_modifier.php'; + + $this->compiler->prepareFile($file); + + $classes = $this->getProperty('classes'); + $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']); + } + + public function testPrepareFileInfersEachClassConstantTypeIndependently(): void + { + $file = __DIR__ . '/../code/preprocessor/class_constants.php'; + + $this->compiler->prepareFile($file); + + $classes = $this->getProperty('classes'); + $this->assertArrayHasKey('preprocessorclassconstants', $classes); + $constants = $classes['preprocessorclassconstants']->constants; + $this->assertSame('php::Str', $constants['TEXT']->type); + $this->assertSame('php::Array', $constants['ITEMS']->type); + } + + public function testPrepareFileRejectsDuplicateClassConstants(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Duplicate constant `VALUE`'); + + $file = __DIR__ . '/../code/preprocessor/duplicate_class_constant.php'; + $this->compiler->prepareFile($file); + } + + public function testPrepareFileParsesAbstractMethodSignatures(): void + { + $file = __DIR__ . '/../code/preprocessor/abstract_method_signature.php'; + + $this->compiler->prepareFile($file); + + $classes = $this->getProperty('classes'); + $this->assertArrayHasKey('preprocessorabstractsignature', $classes); + $methodDef = $classes['preprocessorabstractsignature']->abstractMethodDefs['load']; + $functionDef = $methodDef->functionDef; + $this->assertTrue($functionDef->method); + $this->assertSame('php::Int', $functionDef->argInfoList[0]->type); + $this->assertSame('php::Var', $functionDef->argInfoList[1]->type); + $this->assertNotEmpty($functionDef->argInfoList[1]->typeCheck); + $this->assertSame('php::Object', $functionDef->returnType); + $this->assertSame('PreprocessorAbstractSignature', $functionDef->returnClass); + } + + public function testPrepareFileRejectsDuplicateAbstractMethods(): void + { + $this->expectException(TestError::class); + $this->expectExceptionMessage('Duplicate method `load`'); + + $file = __DIR__ . '/../code/preprocessor/duplicate_abstract_method.php'; + $this->compiler->prepareFile($file); + } + public function testIntersectionParamDeclFallsBackToVarWithRuntimeCheck(): void { $fn = $this->parseFunctionNode('getNamespacedClassName($implement); + $interfaceName = $this->getNamespacedClassName($this->parseIdentifier($implement)); + $list[] = $interfaceName; + if (!$this->isInternalInterface($interfaceName)) { + $this->symbolCallInFile[$this->file][] = strtolower($interfaceName); + } } return $list; } @@ -1822,6 +1826,11 @@ class CompilerBase extends \PhpAot\Core\Translator return array_key_exists($this->escapeClass($name), $this->interfaces); } + protected function getInterface(string $name): InterfaceDef + { + return $this->interfaces[$this->escapeClass($name)]; + } + protected function checkFunction(string $name): void { // 在预处理阶段检测到函数声明,但是未定义,说明在当前文件,但是顺序错误 @@ -3578,7 +3587,7 @@ class CompilerBase extends \PhpAot\Core\Translator if ($this->isNameExpr($expr->name) and $this->hasConstant($name)) { return $this->getConstant($name); } - if ($this->namespace and $this->isNameExpr($expr->name) and !str_contains($name, '\\')) { + if ($this->namespace and $this->isNameExpr($expr->name) and !$expr->name instanceof Node\Name\FullyQualified) { $nsName = $this->namespace . '\\' . $name; if ($this->hasConstant($nsName)) { return $this->getConstant($nsName); @@ -3725,12 +3734,19 @@ class CompilerBase extends \PhpAot\Core\Translator } // Check transitive interface inheritance (e.g., Iterator extends Traversable) foreach ($classDef->implements as $iface) { - $check = $iface; - while ($check && $this->hasInterface($check)) { + $stack = [$iface]; + while ($stack) { + $check = array_pop($stack); if (strcasecmp($check, $expected) === 0) { return true; } - $check = $this->getInterface($check)->extends; + if (!$this->hasInterface($check)) { + continue; + } + $interfaceDef = $this->getInterface($check); + foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parentIface) { + $stack[] = $parentIface; + } } if (is_subclass_of($iface, $expected)) { return true; diff --git a/src/Php/Entity/ClassDef.php b/src/Php/Entity/ClassDef.php index 96a13f5b..0e23e0ee 100644 --- a/src/Php/Entity/ClassDef.php +++ b/src/Php/Entity/ClassDef.php @@ -48,6 +48,12 @@ class ClassDef extends ClassLikeDef * @var array */ public array $abstractMethods = []; + + /** + * Abstract method name (lowercase) => method definition + * @var array + */ + public array $abstractMethodDefs = []; public ?Trait_ $trait = null; /** @@ -79,9 +85,13 @@ class ClassDef extends ClassLikeDef $this->methods[strtolower($method->name)] = $method; } - public function addAbstractMethod(string $name, int $flags): void + public function addAbstractMethod(string $name, int $flags, ?MethodDef $methodDef = null): void { - $this->abstractMethods[strtolower($name)] = $flags; + $lower = strtolower($name); + $this->abstractMethods[$lower] = $flags; + if ($methodDef !== null) { + $this->abstractMethodDefs[$lower] = $methodDef; + } } public function hasMethod(string $method): bool diff --git a/src/Php/Entity/InterfaceDef.php b/src/Php/Entity/InterfaceDef.php index 019b4e9f..a5fef3f2 100644 --- a/src/Php/Entity/InterfaceDef.php +++ b/src/Php/Entity/InterfaceDef.php @@ -10,8 +10,38 @@ namespace PhpAot\Php\Entity; class InterfaceDef extends ClassLikeDef { + /** + * @var array + */ + public array $methods = []; + + /** + * @var array + */ + public array $constants = []; + + /** + * @var string[] + */ + public array $extendsList = []; + public function __construct(string $name, string $namespace = '') { parent::__construct($name, $namespace); } + + public function addMethod(MethodDef $method): void + { + $this->methods[strtolower($method->name)] = $method; + } + + public function hasMethod(string $method): bool + { + return isset($this->methods[strtolower($method)]); + } + + public function hasConstant(string $name): bool + { + return isset($this->constants[$name]); + } } diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index c9bc6693..d2054afb 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -212,6 +212,7 @@ class Preprocessor extends CompilerBase $this->parseGroupUse($v2); break; case 'Stmt_Const': + $this->parseConstDef($v2); break; case 'Stmt_Interface': $this->parseInterface($v2); @@ -554,13 +555,10 @@ class Preprocessor extends CompilerBase $flags = $this->parseModifiers($v->flags); $class = ''; - if ($v->type) { - $type = $this->parseTypeDecl($v->type, self::DECL_TYPE_OF_CONST, $class); - } else { - $type = null; - } + $declaredType = $v->type ? $this->parseTypeDecl($v->type, self::DECL_TYPE_OF_CONST, $class) : null; foreach ($v->consts as $const) { + $type = $declaredType; if ($type === null) { $type = match ($const->value->getType()) { 'Expr_Array' => self::TYPE_ARRAY, @@ -569,6 +567,9 @@ class Preprocessor extends CompilerBase }; } $constName = $this->parseIdentifier($const->name); + if ($this->classDef->hasConstant($constName)) { + $this->fatalError($v, "Duplicate constant `{$constName}`"); + } $constValue = $this->parseIdentifier($const->value); $constInfo = new ConstantDef($constName, $flags, $type, $constValue); @@ -654,9 +655,16 @@ class Preprocessor extends CompilerBase $this->checkRequiredArgNum($name, $this->methodDef, $v); $this->classDef->addMethod($this->methodDef); } else { + if ($this->classDef->hasMethod($name) || $this->classDef->hasAbstractMethod($name)) { + $this->fatalError($v, "Duplicate method `{$this->method}`"); + } if (!$class instanceof Node\Stmt\Trait_ && isset($class->flags) && !($class->flags & Modifiers::ABSTRACT)) { $this->fatalError($v, "Non-abstract class {$this->class} contains abstract method {$v->name}"); } + $this->methodDef = new MethodDef($flags, $name); + $this->methodDef->functionDef = $this->parseFunctionDecl($v); + $this->methodDef->functionDef->method = true; + $this->checkRequiredArgNum($name, $this->methodDef, $v); if ($this->method === '__construct') { foreach ($v->params as $param) { if ($param->isPromoted()) { @@ -664,7 +672,7 @@ class Preprocessor extends CompilerBase } } } - $this->classDef->addAbstractMethod($name, $flags); + $this->classDef->addAbstractMethod($name, $flags, $this->methodDef); } $fullClassName = $this->getFullClassName(); @@ -716,12 +724,81 @@ class Preprocessor extends CompilerBase protected function parseInterface(Node\Stmt\Interface_ $v): void { + $this->resetClass(); + $this->resetMethod(); + $this->resetFunction(); $name = $this->parseIdentifier($v->name); $this->interface = $name; $this->interfaceDef = new InterfaceDef($name, $this->namespace); - $interfaceName = $this->interfaceDef->getNamespacedName(); + $interfaceName = $this->interfaceDef->getNamespacedName(false); + $interfaceNameLower = strtolower($interfaceName); + + foreach ($v->extends as $parent) { + $parentName = $this->getNamespacedClassName($this->parseIdentifier($parent)); + $this->interfaceDef->extendsList[] = $parentName; + if ($this->interfaceDef->extends === '') { + $this->interfaceDef->extends = $parentName; + } + if (!$this->isInternalInterface($parentName)) { + $this->symbolCallInFile[$this->file][] = strtolower($parentName); + } + } + + if (isset($this->symbolDeclInFile[$interfaceNameLower])) { + $this->fatalError($v, "Duplicate interface `{$interfaceName}`"); + } + + $this->symbolDeclInFile[$interfaceNameLower] = $this->file; $this->interfaces[$this->escapeClass($interfaceName)] = $this->interfaceDef; $this->interfacesDefineInFile[$interfaceName] = $this->interfaceDef; + + foreach ($v->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\ClassConst) { + foreach ($stmt->consts as $const) { + $constName = $this->parseIdentifier($const->name); + if ($this->interfaceDef->hasConstant($constName)) { + $this->fatalError($stmt, "Duplicate constant `{$constName}`"); + } + $class = ''; + $type = $stmt->type + ? $this->parseTypeDecl($stmt->type, self::DECL_TYPE_OF_CONST, $class) + : match ($const->value->getType()) { + 'Expr_Array' => self::TYPE_ARRAY, + '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; + $this->interfaceDef->constants[$constName] = $constInfo; + } + continue; + } + + if ($stmt instanceof Node\Stmt\ClassMethod) { + $methodName = $this->getMethodName($stmt); + if ($this->interfaceDef->hasMethod($methodName)) { + $this->fatalError($stmt, "Duplicate method `{$methodName}`"); + } + $this->method = $methodName; + $methodDef = new MethodDef($this->parseModifiers($stmt->flags), $methodName); + $methodDef->functionDef = $this->parseFunctionDecl($stmt); + $methodDef->functionDef->method = true; + $this->interfaceDef->addMethod($methodDef); + $this->resetMethod(); + $this->resetFunction(); + continue; + } + + if (!$stmt instanceof Node\Stmt\Nop) { + $this->fatalError($stmt, 'Unsupported interface statement: ' . $stmt->getType()); + } + } + + $this->resetMethod(); + $this->resetFunction(); + $this->interface = ''; + $this->interfaceDef = null; } protected function parseTraitUseOptions(Node\Stmt\TraitUse $traitUse, array &$aliases, array &$ignored): void @@ -739,7 +816,7 @@ class Preprocessor extends CompilerBase $traits[] = $adaptation->trait; } foreach ($traits as $trait) { - $traitName = $this->getNamespacedClassName($trait); + $traitName = $this->getNamespacedClassName($this->parseIdentifier($trait)); $methodName = $adaptation->method->toString(); /* * 例如: @@ -747,7 +824,7 @@ class Preprocessor extends CompilerBase * 这表示 TraitA::method() 会被重命名为 TraitA::newMethod() */ $aliases[$this->getFullMethodName($traitName, $methodName)] = [ - 'newName' => $adaptation->newName->toString(), + 'newName' => $adaptation->newName ? $adaptation->newName->toString() : $methodName, 'newModifier' => $adaptation->newModifier ?: 0, ]; } @@ -763,7 +840,8 @@ class Preprocessor extends CompilerBase * 这表示 TraitB::method() 将会被忽略,真正执行的是 TraitA::method() */ foreach ($adaptation->insteadof as $trait2) { - $ignored[$this->getFullMethodName($trait2, $methodName)] = true; + $traitName = $this->getNamespacedClassName($this->parseIdentifier($trait2)); + $ignored[$this->getFullMethodName($traitName, $methodName)] = true; } } } @@ -776,6 +854,12 @@ class Preprocessor extends CompilerBase if ($v->adaptations) { $this->parseTraitUseOptions($v, $aliases, $ignored); } + foreach ($v->traits as $trait) { + $traitName = $this->getNamespacedClassName($this->parseIdentifier($trait)); + if (!$this->isInternalClass($traitName)) { + $this->symbolCallInFile[$this->file][] = strtolower($traitName); + } + } $this->classDef->traitAliases = array_merge($this->classDef->traitAliases, $aliases); $this->classDef->traitIgnored = array_merge($this->classDef->traitIgnored, $ignored); } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index dc5401cc..0a976bc7 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -1864,14 +1864,17 @@ CODE; protected function getRegisterClassFunctionCeList(ClassDef|InterfaceDef $classDef): array { $list = []; + if ($classDef instanceof InterfaceDef) { + foreach ($classDef->extendsList ?: ($classDef->extends ? [$classDef->extends] : []) as $parentInterface) { + $list[] = self::PREFIX . 'class_entry_' . $this->escapeCeName($parentInterface); + } + return $list; + } + $parentCe = $this->getParentClassCe($classDef); if ($parentCe !== '') { $list = [$parentCe]; } - // interface 没有 implements - if ($classDef instanceof InterfaceDef) { - return $list; - } $implements = $this->getImplementCe($classDef); return array_merge($list, $implements); @@ -2290,14 +2293,13 @@ CODE; $sorter = new StringSort(); foreach ($this->interfaces as $interfaceDef) { - $parent = $interfaceDef->extends; $ce = $this->getClassCe($interfaceDef); $deps = []; - if ($parent) { + foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parent) { + $tmpCe = self::PREFIX . 'class_entry_' . $this->escapeCeName($parent); // 不存在的接口,说明可能是内置接口 - $tmpCe = $this->getParentClassCe($interfaceDef); - if (!isset($this->interfaces[$parent])) { + if (!$this->hasInterface($parent)) { $sorter->add($tmpCe); } $deps[] = $tmpCe; @@ -2735,6 +2737,9 @@ CODE; break; } } + if (!$class instanceof Node\Stmt\Trait_) { + $this->checkInterfaceImplementations($class); + } $code = $this->genNativeMethod($methodCodes); $oriCtx = $this->context; @@ -3084,17 +3089,16 @@ CODE; 'Cannot override private method `' . $extends . '::' . $name . '()`'); } - $parentFuncDef = $methodDef->functionDef; - $this->validateMethodOverrideSignature($v, $name, $childFuncDef, $methodDef, $extends); + $this->validateMethodOverrideSignature($v, $name, $this->methodDef, $methodDef, $extends); break; } } } private function validateMethodOverrideSignature( - Node\Stmt\ClassMethod $v, + NodeAbstract $v, string $methodName, - FunctionDef $childFuncDef, + MethodDef $childMethodDef, MethodDef $parentMethodDef, string $parentClass ): void { @@ -3107,12 +3111,13 @@ CODE; // PHP allows widening visibility in overrides (e.g. protected -> public), // but forbids narrowing it. - if ($this->getVisibilityRank($this->methodDef->flags) < $this->getVisibilityRank($parentMethodDef->flags)) { + if ($this->getVisibilityRank($childMethodDef->flags) < $this->getVisibilityRank($parentMethodDef->flags)) { $error('visibility mismatch'); } + $childFuncDef = $childMethodDef->functionDef; $parentFuncDef = $parentMethodDef->functionDef; - if (!$parentFuncDef) { + if (!$childFuncDef || !$parentFuncDef) { return; } @@ -3154,6 +3159,57 @@ CODE; } } + private function checkInterfaceImplementations(Node\Stmt\Class_|Node\Stmt\Enum_ $classStmt): void + { + $classDef = $this->classDef; + foreach ($classDef->implements as $interfaceName) { + $this->checkInterfaceImplementation($classStmt, $classDef, $interfaceName); + } + } + + private function checkInterfaceImplementation(NodeAbstract $node, ClassDef $classDef, string $interfaceName): void + { + if ($this->isInternalInterface($interfaceName)) { + return; + } + if (!$this->hasInterface($interfaceName)) { + return; + } + + $interfaceDef = $this->getInterface($interfaceName); + foreach ($interfaceDef->methods as $methodName => $interfaceMethodDef) { + $childMethodDef = $this->findClassMethodDef($classDef, $methodName); + if ($childMethodDef === null) { + $this->fatalError($node, "Class `{$classDef->getNamespacedName(false)}` must implement method `{$interfaceName}::{$interfaceMethodDef->name}()`"); + } + $this->validateMethodOverrideSignature( + $node, + $interfaceMethodDef->name, + $childMethodDef, + $interfaceMethodDef, + $interfaceName + ); + } + + foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parentInterface) { + $this->checkInterfaceImplementation($node, $classDef, $parentInterface); + } + } + + private function findClassMethodDef(ClassDef $classDef, string $methodName): ?MethodDef + { + $current = $classDef; + while (true) { + if ($current->hasMethod($methodName)) { + return $current->getMethod($methodName); + } + if (!$current->extends || !$this->hasClass($current->extends)) { + return null; + } + $current = $this->getClass($current->extends); + } + } + private function getVisibilityRank(int $flags): int { if ($flags & Modifiers::PUBLIC) { diff --git a/tests/aot/namespace/ns-const.phpt b/tests/aot/namespace/ns-const-01.phpt similarity index 100% rename from tests/aot/namespace/ns-const.phpt rename to tests/aot/namespace/ns-const-01.phpt diff --git a/tests/aot/namespace/ns-const-02.phpt b/tests/aot/namespace/ns-const-02.phpt new file mode 100644 index 00000000..247e0dda --- /dev/null +++ b/tests/aot/namespace/ns-const-02.phpt @@ -0,0 +1,28 @@ +--TEST-- +preprocessor registers namespace constants +--FILE-- + +--EXPECT-- +string(12) "ns-const:123" +string(8) "ns-const" +int(123) diff --git a/tests/aot/namespace/ns-const-03.phpt b/tests/aot/namespace/ns-const-03.phpt new file mode 100644 index 00000000..9d3b6c7d --- /dev/null +++ b/tests/aot/namespace/ns-const-03.phpt @@ -0,0 +1,24 @@ +--TEST-- +namespace relative qualified constants +--FILE-- + +--EXPECT-- +int(77) +int(77)