diff --git a/phpunit/code/inheritance_error_final_method.php b/phpunit/code/inheritance_error_final_method.php new file mode 100644 index 00000000..b2f62163 --- /dev/null +++ b/phpunit/code/inheritance_error_final_method.php @@ -0,0 +1,15 @@ +exec('must be compatible', 'inheritance_error_return.php'); } + public function testReturnTypeCannotBeContravariant() + { + $this->exec('must be compatible', 'inheritance_error_return_contravariant_class.php'); + } + + public function testParameterTypeCannotBeCovariant() + { + $this->exec('must be compatible', 'inheritance_error_param_covariant_class.php'); + } + public function testByRefMismatch() { $this->exec('must be compatible', 'inheritance_error_byref.php'); @@ -73,6 +83,11 @@ class InheritanceErrorTest extends TestCase $this->exec('must be compatible', 'inheritance_error_static.php'); } + public function testCannotOverrideFinalMethod() + { + $this->exec('Cannot override final method', 'inheritance_error_final_method.php'); + } + public function testInterfaceMethodStaticMismatch() { $this->exec('must be compatible', 'interface_method_static_mismatch.php'); @@ -83,6 +98,21 @@ class InheritanceErrorTest extends TestCase $this->assertCompiles('inheritance_optional_param_allowed.php'); } + public function testChildMayDeclareReturnTypeWhenParentHasNone() + { + $this->assertCompiles('inheritance_return_type_covariant_from_none.php'); + } + + public function testChildReturnTypeMayBeCovariant() + { + $this->assertCompiles('inheritance_return_type_covariant_class.php'); + } + + public function testChildParameterTypeMayBeContravariant() + { + $this->assertCompiles('inheritance_parameter_type_contravariant_class.php'); + } + public function testPropertyTypeMismatch() { $this->exec('must be compatible', 'inheritance_error_prop_type.php'); diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 7a69a90e..cfe04d6c 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -3159,9 +3159,13 @@ CODE; } // 父类是内置类 if ($classDef->inheritedFromInternalClass) { - if (Reflection::getClassMethodModifiers($extends, $name) & \ReflectionMethod::IS_PRIVATE) { + $modifiers = Reflection::getClassMethodModifiers($extends, $name); + if ($modifiers & \ReflectionMethod::IS_PRIVATE) { goto _error; } + if ($modifiers & \ReflectionMethod::IS_FINAL) { + goto _final_error; + } break; } $classDef = $this->getClass($extends); @@ -3173,6 +3177,12 @@ CODE; 'Cannot override private method `' . $extends . '::' . $name . '()`'); } + if ($methodDef->flags & Modifiers::FINAL) { + _final_error: + $this->fatalError($v, + 'Cannot override final method `' . + $extends . '::' . $name . '()`'); + } $this->validateMethodOverrideSignature($v, $name, $this->methodDef, $methodDef, $extends); break; } @@ -3213,9 +3223,7 @@ CODE; return; } - // Compare return type - if ($childFuncDef->returnType !== $parentFuncDef->returnType || - $childFuncDef->returnClass !== $parentFuncDef->returnClass) { + if (!$this->isReturnTypeOverrideCompatible($childFuncDef, $parentFuncDef)) { $error('return type mismatch'); } @@ -3231,7 +3239,7 @@ CODE; $error("missing parameter #{$i}"); } $childArg = $childFuncDef->argInfoList[$i]; - if ($childArg->type !== $parentArg->type || $childArg->class !== $parentArg->class) { + if (!$this->isParameterTypeOverrideCompatible($childArg, $parentArg)) { $error("parameter #{$i} type mismatch"); } if ($childArg->byRef !== $parentArg->byRef) { @@ -3251,6 +3259,61 @@ CODE; } } + private function isReturnTypeOverrideCompatible(FunctionDef $childFuncDef, FunctionDef $parentFuncDef): bool + { + if ($parentFuncDef->returnTypeUndeclared) { + return true; + } + if ($childFuncDef->returnTypeUndeclared) { + return false; + } + if ($parentFuncDef->returnTypeCheck || $childFuncDef->returnTypeCheck) { + return $parentFuncDef->returnTypeStr === $childFuncDef->returnTypeStr; + } + if ($parentFuncDef->returnType === self::TYPE_VAR) { + return true; + } + if ($childFuncDef->returnType !== $parentFuncDef->returnType) { + return false; + } + if ($parentFuncDef->returnType !== self::TYPE_OBJECT) { + return true; + } + if ($childFuncDef->returnClass === $parentFuncDef->returnClass) { + return true; + } + if (!$childFuncDef->returnClass || !$parentFuncDef->returnClass) { + return false; + } + return $this->isInheritedFrom($childFuncDef->returnClass, $parentFuncDef->returnClass); + } + + private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool + { + if ($parentArg->typeCheck || $childArg->typeCheck) { + return $parentArg->typeStr === $childArg->typeStr; + } + if ($childArg->undeclared || $childArg->type === self::TYPE_VAR) { + return true; + } + if ($parentArg->undeclared || $parentArg->type === self::TYPE_VAR) { + return false; + } + if ($childArg->type !== $parentArg->type) { + return false; + } + if ($parentArg->type !== self::TYPE_OBJECT) { + return true; + } + if ($childArg->class === $parentArg->class) { + return true; + } + if (!$childArg->class || !$parentArg->class) { + return false; + } + return $this->isInheritedFrom($parentArg->class, $childArg->class); + } + private function checkInterfaceImplementations(Node\Stmt\Class_|Node\Stmt\Enum_ $classStmt): void { $classDef = $this->classDef;