fix(compiler): 修复返回类型协变检查

pull/30/head
Yurun 1 month ago
parent 2287695b44
commit c3b24fdd32
  1. 130
      src/Translator.php
  2. 47
      tests/compiler/class/interface-return-covariance.phpt
  3. 51
      tests/compiler/type_decl/return-type-covariance.phpt

@ -3717,25 +3717,135 @@ CODE;
if ($childFuncDef->returnTypeUndeclared) {
return false;
}
if ($parentFuncDef->returnTypeCheck || $childFuncDef->returnTypeCheck) {
return $parentFuncDef->returnTypeStr === $childFuncDef->returnTypeStr;
}
// A parent that accepts everything (mixed/var) is compatible with any
// child return type.
if ($parentFuncDef->returnType === Type::VAR) {
return true;
}
if ($childFuncDef->returnType !== $parentFuncDef->returnType) {
return false;
$parentTypes = $this->getReturnAcceptedTypes($parentFuncDef);
$childTypes = $this->getReturnAcceptedTypes($childFuncDef);
// Return type covariance: every value the child can return must also be
// acceptable under the parent's declared return type. This allows a
// child to narrow a nullable/union return type (e.g. `?Base` -> `?Child`
// or `int|string` -> `int`) while still satisfying the parent contract.
return $this->isReturnTypeSubtype($childTypes, $parentTypes);
}
private function getReturnAcceptedTypes(FunctionDef $functionDef): array
{
if (!empty($functionDef->returnTypeCheck)) {
return $functionDef->returnTypeCheck;
}
if ($parentFuncDef->returnType !== Type::OBJECT) {
return true;
$type = $functionDef->returnType;
if ($type === Type::VAR) {
return [['kind' => 'isMixed']];
}
if ($type === Type::OBJECT) {
return $functionDef->returnClass
? [['kind' => 'instanceof', 'class' => $functionDef->returnClass]]
: [['kind' => 'isObject']];
}
return match ($type) {
Type::INT => [['kind' => 'isInt']],
Type::FLOAT => [['kind' => 'isFloat']],
Type::BOOL => [['kind' => 'isBool']],
Type::STR => [['kind' => 'isString']],
Type::ARRAY => [['kind' => 'isArray']],
Type::RESOURCE => [['kind' => 'isResource']],
default => [['kind' => 'isMixed']],
};
}
private function isReturnTypeSubtype(array $childTypes, array $parentTypes): bool
{
foreach ($childTypes as $childType) {
if (!$this->isReturnTypeCoveredBy($childType, $parentTypes)) {
return false;
}
}
if ($childFuncDef->returnClass === $parentFuncDef->returnClass) {
return true;
}
private function isReturnTypeCoveredBy(array $childType, array $parentTypes): bool
{
$childKind = $childType['kind'] ?? null;
// Child is an intersection (A&B): it is a subtype only if every member
// is individually a subtype of the parent type.
if ($childKind === 'allOf') {
foreach ($childType['types'] as $member) {
if (!$this->isReturnTypeCoveredBy($member, $parentTypes)) {
return false;
}
}
return true;
}
if (!$childFuncDef->returnClass || !$parentFuncDef->returnClass) {
foreach ($parentTypes as $parentType) {
$parentKind = $parentType['kind'] ?? null;
// Parent is an intersection (A&B): the child must be a subtype of
// every member of the intersection.
if ($parentKind === 'allOf') {
$ok = true;
foreach ($parentType['types'] as $member) {
if (!$this->isReturnTypeCoveredBy($childType, [$member])) {
$ok = false;
break;
}
}
if ($ok) {
return true;
}
continue;
}
if ($this->isReturnTypeEntryCompatible($childKind, $childType, $parentKind, $parentType)) {
return true;
}
}
return false;
}
private function isReturnTypeEntryCompatible(
?string $childKind,
array $childType,
?string $parentKind,
array $parentType
): bool {
if ($childKind === 'isNull') {
// A null value is only compatible with a nullable (isNull) parent.
return $parentKind === 'isNull';
}
if ($childKind === 'isObject') {
// Any object is compatible with a parent that accepts any object.
return $parentKind === 'isObject';
}
if ($childKind === 'isMixed') {
return $parentKind === 'isMixed';
}
if ($childKind === 'instanceof') {
if ($parentKind === 'isObject') {
return true;
}
if ($parentKind === 'instanceof') {
$childClass = $childType['class'] ?? '';
$parentClass = $parentType['class'] ?? '';
if ($childClass === '' || $parentClass === '' || $childClass === 'static' || $parentClass === 'static') {
return false;
}
if ($childClass === $parentClass) {
return true;
}
return $this->isInheritedFrom($childClass, $parentClass);
}
return false;
}
return $this->isInheritedFrom($childFuncDef->returnClass, $parentFuncDef->returnClass);
// Scalar kinds must match exactly.
return $childKind === $parentKind;
}
private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool

@ -0,0 +1,47 @@
--TEST--
Interface return type covariance with nullable interface and anonymous class
--FILE--
<?php
declare(strict_types=1);
interface TestInterface1
{
}
interface TestInterface2 extends TestInterface1
{
}
interface TestInterface3
{
public function test(): ?TestInterface1;
}
class TestClass implements TestInterface3
{
// Covariant: ?TestInterface2 is a subtype of ?TestInterface1 because
// TestInterface2 extends TestInterface1.
public function test(): ?TestInterface2
{
return new class() implements TestInterface2 {
public function hello(): string {
return "anon";
}
};
}
}
function main()
{
$test = new TestClass;
$result = $test->test();
var_dump($result instanceof TestInterface1);
var_dump($result instanceof TestInterface2);
var_dump($result === null);
}
?>
--EXPECT--
bool(true)
bool(true)
bool(false)

@ -0,0 +1,51 @@
--TEST--
Return type covariance: union narrowing and object subtype
--FILE--
<?php
interface UnionReturnContract
{
public function make(): int|string;
}
class UnionReturnImpl implements UnionReturnContract
{
// Covariant: narrowing a union return type (int|string -> int) is allowed.
public function make(): int
{
return 42;
}
}
class BaseType {}
class ChildType extends BaseType {}
interface ObjectReturnContract
{
public function build(): BaseType;
}
class ObjectReturnImpl implements ObjectReturnContract
{
// Covariant: returning a subtype (ChildType) for a BaseType return is allowed.
public function build(): ChildType
{
return new ChildType();
}
}
function main()
{
$impl = new UnionReturnImpl();
var_dump($impl->make());
$obj = new ObjectReturnImpl();
$built = $obj->build();
var_dump($built instanceof BaseType);
var_dump($built instanceof ChildType);
}
?>
--EXPECT--
int(42)
bool(true)
bool(true)
Loading…
Cancel
Save