refactor(php): 重构PHP类定义和接口实现检查逻辑

- 添加抽象方法签名兼容性检查功能
- 实现接口常量初始化表达式解析支持
- 优化类和接口常量处理流程
- 增强继承关系中抽象方法实现验证
- 改进接口实现方法查找逻辑
- 添加抽象类可延迟实现接口方法的支持
- 修复方法重写签名验证问题
- 重构常量解析为统一方法处理
- 更新类型检查生成器去除排序逻辑
- 扩展测试用例覆盖各种继承场景
pull/5/head
韩天峰 2 months ago
parent 3ca0d64a6a
commit c4a8146661
  1. 15
      phpunit/code/abstract_method_signature_mismatch.php
  2. 11
      phpunit/code/abstract_parent_method_missing.php
  3. 19
      phpunit/code/interface_abstract_class_missing.php
  4. 12
      phpunit/code/interface_abstract_method_mismatch.php
  5. 20
      phpunit/code/interface_abstract_method_signature.php
  6. 15
      phpunit/code/interface_abstract_parent_missing.php
  7. 7
      phpunit/code/interface_array_constant.php
  8. 44
      phpunit/src/InheritanceErrorTest.php
  9. 14
      phpunit/src/PreprocessorTest.php
  10. 5
      src/Php/Entity/ClassDef.php
  11. 2
      src/Php/Generator/TypeCheckGenerator.php
  12. 16
      src/Php/Preprocessor.php
  13. 77
      src/Php/Translator.php

@ -0,0 +1,15 @@
<?php
abstract class AbstractSignatureBase
{
abstract public function run(int $id): string;
}
class AbstractSignatureChild extends AbstractSignatureBase
{
public function run(string $id): string
{
return $id;
}
}
function main() {}

@ -0,0 +1,11 @@
<?php
abstract class AbstractParentMissing
{
abstract public function run(): void;
}
class ConcreteParentMissing extends AbstractParentMissing
{
}
function main() {}

@ -0,0 +1,19 @@
<?php
interface ContractAbstractMissing
{
public function run(int $id): string;
}
abstract class AbstractImplMissing implements ContractAbstractMissing
{
}
class ConcreteImplMissing extends AbstractImplMissing
{
public function run(int $id): string
{
return (string) $id;
}
}
function main() {}

@ -0,0 +1,12 @@
<?php
interface ContractAbstractMismatch
{
public function run(int $id): string;
}
abstract class AbstractImplMismatch implements ContractAbstractMismatch
{
abstract public function run(string $id): string;
}
function main() {}

@ -0,0 +1,20 @@
<?php
interface ContractAbstractMethod
{
public function run(int $id): string;
}
abstract class AbstractImplMethod implements ContractAbstractMethod
{
abstract public function run(int $id): string;
}
class ConcreteImplMethod extends AbstractImplMethod
{
public function run(int $id): string
{
return (string) $id;
}
}
function main() {}

@ -0,0 +1,15 @@
<?php
interface ContractAbstractParentMissing
{
public function run(): void;
}
abstract class AbstractContractParentMissing implements ContractAbstractParentMissing
{
}
class ConcreteContractParentMissing extends AbstractContractParentMissing
{
}
function main() {}

@ -0,0 +1,7 @@
<?php
interface InterfaceArrayConstant
{
public const ITEMS = [1, 2, 3];
}
function main() {}

@ -112,4 +112,48 @@ class InheritanceErrorTest extends TestCase
{
$this->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');
}
}

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

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

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

@ -570,6 +570,15 @@ class Preprocessor extends CompilerBase
if ($this->classDef->hasConstant($constName)) {
$this->fatalError($v, "Duplicate constant `{$constName}`");
}
$constInfo = $this->parseClassLikeConstant($const, $flags, $type, $class);
$constInfo->class = $class;
$this->classDef->constants[$constInfo->name] = $constInfo;
}
}
private function parseClassLikeConstant(Node\Const_ $const, int $flags, string $type, string $class = ''): ConstantDef
{
$constName = $this->parseIdentifier($const->name);
$constValue = $this->parseIdentifier($const->value);
$constInfo = new ConstantDef($constName, $flags, $type, $constValue);
@ -584,8 +593,7 @@ class Preprocessor extends CompilerBase
$constInfo->arrayExpr = $arrayExpr;
}
$constInfo->class = $class;
$this->classDef->constants[$constInfo->name] = $constInfo;
}
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;

@ -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<ClassDef|InterfaceDef>
*/
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<string>
*/
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) {

Loading…
Cancel
Save