|
|
|
|
@ -1602,12 +1602,6 @@ CODE; |
|
|
|
|
$list = []; |
|
|
|
|
if ($func->method) { |
|
|
|
|
$list[] = Type::OBJECT . ' &this_'; |
|
|
|
|
// A trait method with `parent::` calls receives an implicit |
|
|
|
|
// `trait_parent_ce` parameter right after `this_`. The definition |
|
|
|
|
// adds it (see parseFunction); the declaration must match. |
|
|
|
|
if ($func->traitParentCe) { |
|
|
|
|
$list[] = 'zend_class_entry *trait_parent_ce'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
$argInfoList = $func->argInfoList; |
|
|
|
|
if ($argInfoList) { |
|
|
|
|
@ -3472,9 +3466,6 @@ CODE; |
|
|
|
|
$functionDeclCode .= Type::OBJECT . ' &this_'; |
|
|
|
|
if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) { |
|
|
|
|
$functionDeclCode .= ', zend_class_entry *trait_parent_ce'; |
|
|
|
|
// Record the implicit parameter so the shared `func_decl.h` |
|
|
|
|
// declaration (genFunctionDeclaration) emits the same signature. |
|
|
|
|
$this->functionDef->traitParentCe = true; |
|
|
|
|
} |
|
|
|
|
if ($this->functionDef->params) { |
|
|
|
|
$functionDeclCode .= ', '; |
|
|
|
|
@ -3635,7 +3626,12 @@ CODE; |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!$this->isReturnTypeOverrideCompatible($childFuncDef, $parentFuncDef)) { |
|
|
|
|
if (!$this->isReturnTypeOverrideCompatible( |
|
|
|
|
$childFuncDef, |
|
|
|
|
$parentFuncDef, |
|
|
|
|
$className, |
|
|
|
|
$parentClass, |
|
|
|
|
)) { |
|
|
|
|
$this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass); |
|
|
|
|
} |
|
|
|
|
if ($childFuncDef->returnsByRef !== $parentFuncDef->returnsByRef) { |
|
|
|
|
@ -3720,170 +3716,176 @@ CODE; |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private function isReturnTypeOverrideCompatible(FunctionDef $childFuncDef, FunctionDef $parentFuncDef): bool |
|
|
|
|
{ |
|
|
|
|
private function isReturnTypeOverrideCompatible( |
|
|
|
|
FunctionDef $childFuncDef, |
|
|
|
|
FunctionDef $parentFuncDef, |
|
|
|
|
string $childClass, |
|
|
|
|
string $parentClass, |
|
|
|
|
): bool { |
|
|
|
|
if ($parentFuncDef->returnTypeUndeclared) { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if ($childFuncDef->returnTypeUndeclared) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
// A parent that accepts everything (mixed/var) is compatible with any |
|
|
|
|
// child return type. |
|
|
|
|
if ($parentFuncDef->returnType === Type::VAR) { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
$parentTypes = $this->getReturnAcceptedTypes($parentFuncDef); |
|
|
|
|
$childTypes = $this->getReturnAcceptedTypes($childFuncDef); |
|
|
|
|
$parentTypes = $this->getReturnAcceptedTypes($parentFuncDef, $parentClass); |
|
|
|
|
$childTypes = $this->getReturnAcceptedTypes($childFuncDef, $childClass); |
|
|
|
|
|
|
|
|
|
// 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); |
|
|
|
|
// Type checks are stored in disjunctive normal form: the outer list is |
|
|
|
|
// a union, while an allOf entry is an intersection. Every child union |
|
|
|
|
// branch must imply at least one complete parent branch. |
|
|
|
|
foreach ($childTypes as $childType) { |
|
|
|
|
if (!$this->isReturnTypeCoveredBy($childType, $parentTypes)) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private function getReturnAcceptedTypes(FunctionDef $functionDef): array |
|
|
|
|
{ |
|
|
|
|
if ($functionDef->generator) { |
|
|
|
|
// The runtime return type of a generator is neutralized to VAR because |
|
|
|
|
// it actually returns a `\FiberGenerator`. Use the source-level declared |
|
|
|
|
// return type so interface/abstract covariance checks still work. |
|
|
|
|
if (!empty($functionDef->declaredReturnTypeCheck)) { |
|
|
|
|
return $functionDef->declaredReturnTypeCheck; |
|
|
|
|
} |
|
|
|
|
$type = $functionDef->declaredReturnType; |
|
|
|
|
if ($type === Type::VAR) { |
|
|
|
|
return [['kind' => 'isMixed']]; |
|
|
|
|
} |
|
|
|
|
if ($type === Type::OBJECT) { |
|
|
|
|
return $functionDef->declaredReturnClass |
|
|
|
|
? [['kind' => 'instanceof', 'class' => $functionDef->declaredReturnClass]] |
|
|
|
|
: [['kind' => 'isObject']]; |
|
|
|
|
} |
|
|
|
|
return match ($type) { |
|
|
|
|
private function getReturnAcceptedTypes(FunctionDef $functionDef, string $declaringClass): array |
|
|
|
|
{ |
|
|
|
|
$returnTypeCheck = $functionDef->generator |
|
|
|
|
? $functionDef->declaredReturnTypeCheck |
|
|
|
|
: $functionDef->returnTypeCheck; |
|
|
|
|
$returnType = $functionDef->generator |
|
|
|
|
? $functionDef->declaredReturnType |
|
|
|
|
: $functionDef->returnType; |
|
|
|
|
$returnClass = $functionDef->generator |
|
|
|
|
? $functionDef->declaredReturnClass |
|
|
|
|
: $functionDef->returnClass; |
|
|
|
|
$returnTypeStr = $functionDef->generator |
|
|
|
|
? $functionDef->declaredReturnTypeStr |
|
|
|
|
: $functionDef->returnTypeStr; |
|
|
|
|
|
|
|
|
|
if (!empty($returnTypeCheck)) { |
|
|
|
|
return array_map( |
|
|
|
|
fn (array $type): array => $this->normalizeReturnTypeEntry($type, $declaringClass), |
|
|
|
|
$returnTypeCheck, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ($functionDef->returnTypeKeyword === 'static') { |
|
|
|
|
return [['kind' => 'isStatic', 'class' => $declaringClass]]; |
|
|
|
|
} |
|
|
|
|
if ($returnType === Type::OBJECT && $returnClass !== '') { |
|
|
|
|
return [['kind' => 'instanceof', 'class' => $returnClass]]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
$declaredType = strtolower($returnTypeStr); |
|
|
|
|
return match ($declaredType) { |
|
|
|
|
'mixed' => [['kind' => 'isMixed']], |
|
|
|
|
'never' => [['kind' => 'isNever']], |
|
|
|
|
'void' => [['kind' => 'isVoid']], |
|
|
|
|
'null' => [['kind' => 'isNull']], |
|
|
|
|
'true' => [['kind' => 'isTrue']], |
|
|
|
|
'false' => [['kind' => 'isFalse']], |
|
|
|
|
'callable' => [['kind' => 'callable']], |
|
|
|
|
'iterable' => [['kind' => 'iterable']], |
|
|
|
|
'object' => [['kind' => 'isObject']], |
|
|
|
|
default => match ($returnType) { |
|
|
|
|
Type::INT => [['kind' => 'isInt']], |
|
|
|
|
Type::FLOAT => [['kind' => 'isFloat']], |
|
|
|
|
Type::BOOL => [['kind' => 'isBool']], |
|
|
|
|
Type::STR => [['kind' => 'isString']], |
|
|
|
|
Type::ARRAY => [['kind' => 'isArray']], |
|
|
|
|
Type::RESOURCE => [['kind' => 'isResource']], |
|
|
|
|
Type::OBJECT => [['kind' => 'isObject']], |
|
|
|
|
default => [['kind' => 'isMixed']], |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!empty($functionDef->returnTypeCheck)) { |
|
|
|
|
return $functionDef->returnTypeCheck; |
|
|
|
|
} |
|
|
|
|
$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 |
|
|
|
|
private function normalizeReturnTypeEntry(array $type, string $declaringClass): array |
|
|
|
|
{ |
|
|
|
|
foreach ($childTypes as $childType) { |
|
|
|
|
if (!$this->isReturnTypeCoveredBy($childType, $parentTypes)) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
if (($type['kind'] ?? null) === 'allOf') { |
|
|
|
|
$type['types'] = array_map( |
|
|
|
|
fn (array $member): array => $this->normalizeReturnTypeEntry($member, $declaringClass), |
|
|
|
|
$type['types'], |
|
|
|
|
); |
|
|
|
|
} elseif (($type['kind'] ?? null) === 'instanceof' && ($type['class'] ?? null) === 'static') { |
|
|
|
|
$type = ['kind' => 'isStatic', 'class' => $declaringClass]; |
|
|
|
|
} |
|
|
|
|
return true; |
|
|
|
|
return $type; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private function isReturnTypeCoveredBy(array $childType, array $parentTypes): bool |
|
|
|
|
{ |
|
|
|
|
$childKind = $childType['kind'] ?? null; |
|
|
|
|
$childClause = ($childType['kind'] ?? null) === 'allOf' |
|
|
|
|
? $childType['types'] |
|
|
|
|
: [$childType]; |
|
|
|
|
|
|
|
|
|
// 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; |
|
|
|
|
} |
|
|
|
|
foreach ($parentTypes as $parentType) { |
|
|
|
|
$parentClause = ($parentType['kind'] ?? null) === 'allOf' |
|
|
|
|
? $parentType['types'] |
|
|
|
|
: [$parentType]; |
|
|
|
|
if ($this->isReturnTypeClauseSubtype($childClause, $parentClause)) { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
private function isReturnTypeClauseSubtype(array $childClause, array $parentClause): bool |
|
|
|
|
{ |
|
|
|
|
foreach ($parentClause as $parentType) { |
|
|
|
|
$covered = false; |
|
|
|
|
foreach ($childClause as $childType) { |
|
|
|
|
if ($this->isReturnTypeEntryCompatible($childType, $parentType)) { |
|
|
|
|
$covered = true; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ($this->isReturnTypeEntryCompatible($childKind, $childType, $parentKind, $parentType)) { |
|
|
|
|
return true; |
|
|
|
|
if (!$covered) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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'; |
|
|
|
|
private function isReturnTypeEntryCompatible(array $childType, array $parentType): bool |
|
|
|
|
{ |
|
|
|
|
$childKind = $childType['kind'] ?? null; |
|
|
|
|
$parentKind = $parentType['kind'] ?? null; |
|
|
|
|
|
|
|
|
|
if ($childKind === 'isNever' || $parentKind === 'isMixed') { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if (($childKind === 'isTrue' || $childKind === 'isFalse') && $parentKind === 'isBool') { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if ($childKind === 'isObject') { |
|
|
|
|
// Any object is compatible with a parent that accepts any object. |
|
|
|
|
return $parentKind === 'isObject'; |
|
|
|
|
if ($childKind === 'isArray' && $parentKind === 'iterable') { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if ($childKind === 'isMixed') { |
|
|
|
|
return $parentKind === 'isMixed'; |
|
|
|
|
if ($childKind === 'isStatic') { |
|
|
|
|
if ($parentKind === 'isObject' || $parentKind === 'isStatic') { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if ($parentKind === 'instanceof') { |
|
|
|
|
return $this->isInheritedFrom( |
|
|
|
|
$childType['class'] ?? '', |
|
|
|
|
$parentType['class'] ?? '', |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
if ($childKind === 'instanceof') { |
|
|
|
|
if ($parentKind === 'isObject') { |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
$childClass = $childType['class'] ?? ''; |
|
|
|
|
if ($parentKind === 'iterable') { |
|
|
|
|
return $childClass !== '' && $this->isInheritedFrom($childClass, 'Traversable'); |
|
|
|
|
} |
|
|
|
|
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 $childClass !== '' |
|
|
|
|
&& $parentClass !== '' |
|
|
|
|
&& $this->isInheritedFrom($childClass, $parentClass); |
|
|
|
|
} |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
// Scalar kinds must match exactly. |
|
|
|
|
return $childKind === $parentKind; |
|
|
|
|
return $childKind !== null && $childKind === $parentKind; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool |
|
|
|
|
@ -4468,14 +4470,6 @@ CODE; |
|
|
|
|
// function untouched. |
|
|
|
|
$this->reresolveTraitLateBoundTypes($classDef, $methodDef); |
|
|
|
|
|
|
|
|
|
// The wrapper is a method of the *composing* class, not a trait method, so |
|
|
|
|
// it must not receive the implicit `trait_parent_ce` parameter (it computes |
|
|
|
|
// the parent class entry itself when forwarding to the trait function). The |
|
|
|
|
// cloned FunctionDef inherited `traitParentCe` from the trait's FunctionDef; |
|
|
|
|
// clear it so the shared `func_decl.h` declaration matches the wrapper's |
|
|
|
|
// own (2-parameter) definition. |
|
|
|
|
$methodDef->functionDef->traitParentCe = false; |
|
|
|
|
|
|
|
|
|
// Validate `parent::` calls emitted from this trait method against the |
|
|
|
|
// parent of the class that is composing the trait. The trait itself has |
|
|
|
|
// no parent at compile time, so this is the only place the parent class |
|
|
|
|
@ -4546,11 +4540,28 @@ CODE; |
|
|
|
|
private function reresolveTraitLateBoundTypes(ClassDef $usingClassDef, MethodDef $methodDef): void |
|
|
|
|
{ |
|
|
|
|
$fn = $methodDef->functionDef; |
|
|
|
|
// Always produce a distinct FunctionDef for the composing-class wrapper. |
|
|
|
|
// The wrapper is a separate method (different name, and no implicit |
|
|
|
|
// `trait_parent_ce` parameter) from the trait's own function, so it must |
|
|
|
|
// not share the trait's FunctionDef object — mutating one (e.g. clearing |
|
|
|
|
// `traitParentCe`) would otherwise leak into the trait's declaration. |
|
|
|
|
$needsClone = false; |
|
|
|
|
|
|
|
|
|
if ($fn->returnTypeKeyword !== '') { |
|
|
|
|
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); |
|
|
|
|
if ($resolved !== null && $resolved !== $fn->returnClass) { |
|
|
|
|
$needsClone = true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
foreach ($fn->argInfoList as $arg) { |
|
|
|
|
if ($arg->typeKeyword !== '') { |
|
|
|
|
$resolved = $this->resolveLateBoundClass($usingClassDef, $arg->typeKeyword); |
|
|
|
|
if ($resolved !== null && ($resolved !== $arg->class || $resolved !== $arg->declaredClass)) { |
|
|
|
|
$needsClone = true; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!$needsClone) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
$newFn = clone $fn; |
|
|
|
|
if ($fn->returnTypeKeyword !== '') { |
|
|
|
|
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword); |
|
|
|
|
|