merge: sync generator return covariance fix with master

pull/31/head
韩天峰 1 month ago
commit 1698682273
  1. 17
      phpunit/code/constructor_visibility_inherited_private.php
  2. 6
      phpunit/code/constructor_visibility_internal_private.php
  3. 15
      phpunit/code/constructor_visibility_namespaced.php
  4. 11
      phpunit/code/constructor_visibility_private.php
  5. 11
      phpunit/code/constructor_visibility_protected.php
  6. 19
      phpunit/code/constructor_visibility_protected_foreign_class.php
  7. 14
      phpunit/code/inheritance_error_generator_return_widened.php
  8. 23
      phpunit/code/inheritance_error_return_intersection_missing.php
  9. 13
      phpunit/code/inheritance_error_return_never_widened.php
  10. 17
      phpunit/code/inheritance_error_return_static_widened.php
  11. 14
      phpunit/code/inheritance_error_return_union_widened.php
  12. 14
      phpunit/code/preprocessor/namespace_ending_comment_unbracketed.php
  13. 39
      phpunit/code/return_type_covariance_intersection.php
  14. 29
      phpunit/code/trait_constructor_conflict.php
  15. 21
      phpunit/code/trait_constructor_private.php
  16. 21
      phpunit/code/trait_constructor_protected.php
  17. 78
      phpunit/src/ConstructorVisibilityTest.php
  18. 30
      phpunit/src/InheritanceErrorTest.php
  19. 19
      phpunit/src/PreprocessorTest.php
  20. 72
      src/CompilerBase.php
  21. 9
      src/Entity/FunctionDef.php
  22. 1
      src/Generator/FiberGenerator.php
  23. 2
      src/Generator/TypeCheckGenerator.php
  24. 6
      src/Preprocessor.php
  25. 289
      src/Translator.php
  26. 47
      tests/compiler/class/interface-return-covariance.phpt
  27. 14
      tests/compiler/generator/interface-return-type-variants.phpt
  28. 22
      tests/compiler/namespace/namespace-ending-comment.phpt
  29. 26
      tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt
  30. 27
      tests/compiler/trait/trait-ctor-basic.phpt
  31. 32
      tests/compiler/trait/trait-ctor-override.phpt
  32. 37
      tests/compiler/trait/trait-ctor-protected-subclass.phpt
  33. 30
      tests/compiler/trait/trait-ctor-with-args.phpt
  34. 120
      tests/compiler/type_decl/return-type-covariance.phpt

@ -0,0 +1,17 @@
<?php
class PrivateConstructorParent
{
private function __construct()
{
}
}
class PrivateConstructorChild extends PrivateConstructorParent
{
}
function main()
{
new PrivateConstructorChild();
}

@ -0,0 +1,6 @@
<?php
function main()
{
new Closure();
}

@ -0,0 +1,15 @@
<?php
namespace ConstructorVisibility;
class Hidden
{
private function __construct()
{
}
}
function main()
{
new Hidden();
}

@ -0,0 +1,11 @@
<?php
class TestClass
{
private function __construct(){}
}
function main()
{
new TestClass;
}

@ -0,0 +1,11 @@
<?php
class TestClass
{
protected function __construct(){}
}
function main()
{
new TestClass;
}

@ -0,0 +1,19 @@
<?php
class Base
{
protected function __construct(){}
}
class Other
{
public static function make(): Base
{
return new Base();
}
}
function main()
{
Other::make();
}

@ -0,0 +1,14 @@
<?php
interface GeneratorReturnContract
{
public function values(): \Generator;
}
class GeneratorReturnImplementation implements GeneratorReturnContract
{
public function values(): iterable
{
yield 1;
}
}

@ -0,0 +1,23 @@
<?php
interface IntersectionLeft
{
}
interface IntersectionRight
{
}
interface IntersectionReturnParent
{
public function value(): IntersectionLeft&IntersectionRight;
}
class IntersectionReturnChild implements IntersectionReturnParent
{
public function value(): IntersectionLeft
{
return new class implements IntersectionLeft {
};
}
}

@ -0,0 +1,13 @@
<?php
abstract class NeverReturnParent
{
abstract public function stop(): never;
}
abstract class NeverReturnChild extends NeverReturnParent
{
public function stop(): void
{
}
}

@ -0,0 +1,17 @@
<?php
class StaticReturnParent
{
public function value(): static
{
return $this;
}
}
class StaticReturnChild extends StaticReturnParent
{
public function value(): self
{
return $this;
}
}

@ -0,0 +1,14 @@
<?php
interface UnionReturnParent
{
public function value(): int|string;
}
class UnionReturnChild implements UnionReturnParent
{
public function value(): bool
{
return true;
}
}

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace NamespaceEndingComment;
const VALUE = 42;
function value(): int
{
return VALUE;
}
// unbracketed namespace trailing comment

@ -0,0 +1,39 @@
<?php
interface CovarianceLeft
{
}
interface CovarianceRight
{
}
class CovarianceBoth implements CovarianceLeft, CovarianceRight
{
}
interface IntersectionNarrowingContract
{
public function intersection(): CovarianceLeft;
}
class IntersectionNarrowingImpl implements IntersectionNarrowingContract
{
public function intersection(): CovarianceLeft&CovarianceRight
{
return new CovarianceBoth();
}
}
interface IntersectionContract
{
public function concrete(): CovarianceLeft&CovarianceRight;
}
class IntersectionImpl implements IntersectionContract
{
public function concrete(): CovarianceBoth
{
return new CovarianceBoth();
}
}

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
trait TraitA
{
public function __construct()
{
echo "A\n";
}
}
trait TraitB
{
public function __construct()
{
echo "B\n";
}
}
class TestClass
{
use TraitA, TraitB;
}
function main()
{
new TestClass();
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
trait TestTrait
{
private function __construct()
{
echo "trait ctor\n";
}
}
class TestClass
{
use TestTrait;
}
function main()
{
new TestClass();
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
trait TestTrait
{
protected function __construct()
{
echo "trait ctor\n";
}
}
class TestClass
{
use TestTrait;
}
function main()
{
new TestClass();
}

@ -0,0 +1,78 @@
<?php
use TypePhp\Exception\TestError;
class ConstructorVisibilityTest extends BaseTest
{
/**
* 编译期错误在转换阶段直接以 TestError 抛出,而在桩文件生成阶段
* (gen_stub.php) 会被包成 RuntimeException,这里两者都要捕获。
*/
protected function exec(string $expected, string $file): void
{
try {
$this->compile($file);
} catch (TestError|RuntimeException $exception) {
$this->assertStringContainsString($expected, $exception->getMessage());
return;
}
$this->fail('Expected compile-time error was not thrown');
}
public function testPrivateConstructorCannotBeCalledFromOutside(): void
{
// 私有构造器不能从类外部通过 `new` 调用
$this->exec('Cannot call private TestClass::__construct()', 'constructor_visibility_private.php');
}
public function testProtectedConstructorCannotBeCalledFromGlobalScope(): void
{
// 保护构造器不能从全局作用域调用
$this->exec('Cannot call protected TestClass::__construct()', 'constructor_visibility_protected.php');
}
public function testProtectedConstructorCannotBeCalledFromNonSubclass(): void
{
// 保护构造器不能从非子类的其它类内部调用
$this->exec('Cannot call protected Base::__construct()', 'constructor_visibility_protected_foreign_class.php');
}
public function testInheritedPrivateConstructorCannotBeCalledFromChildScope(): void
{
$this->exec(
'Cannot call private PrivateConstructorParent::__construct()',
'constructor_visibility_inherited_private.php'
);
}
public function testInternalPrivateConstructorCannotBeCalled(): void
{
$this->exec('Cannot call private Closure::__construct()', 'constructor_visibility_internal_private.php');
}
public function testNamespacedConstructorUsesPhpClassNameInDiagnostic(): void
{
$this->exec(
'Cannot call private ConstructorVisibility\Hidden::__construct()',
'constructor_visibility_namespaced.php'
);
}
public function testTraitPrivateConstructorCannotBeCalledFromGlobalScope(): void
{
// trait 提供的私有构造器扁平化后等价于类的私有构造器
$this->exec('Cannot call private TestClass::__construct()', 'trait_constructor_private.php');
}
public function testTraitProtectedConstructorCannotBeCalledFromGlobalScope(): void
{
// trait 提供的保护构造器扁平化后等价于类的保护构造器
$this->exec('Cannot call protected TestClass::__construct()', 'trait_constructor_protected.php');
}
public function testConflictingTraitConstructorMustBeResolved(): void
{
// 两个 trait 各自声明 __construct 时必须显式解决冲突
$this->exec('Trait `TraitB` method `__construct` already exists', 'trait_constructor_conflict.php');
}
}

@ -53,6 +53,36 @@ class InheritanceErrorTest extends TestCase
$this->exec('must be compatible', 'inheritance_error_return_contravariant_class.php');
}
public function testUnionReturnTypeCannotBeWidenedToUnrelatedType(): void
{
$this->exec('must be compatible', 'inheritance_error_return_union_widened.php');
}
public function testIntersectionReturnTypeCannotDropAMember(): void
{
$this->exec('must be compatible', 'inheritance_error_return_intersection_missing.php');
}
public function testStaticReturnTypeCannotBeWidenedToSelf(): void
{
$this->exec('must be compatible', 'inheritance_error_return_static_widened.php');
}
public function testNeverReturnTypeCannotBeWidenedToVoid(): void
{
$this->exec('must be compatible', 'inheritance_error_return_never_widened.php');
}
public function testGeneratorReturnTypeCannotBeWidenedToIterable(): void
{
$this->exec('must be compatible', 'inheritance_error_generator_return_widened.php');
}
public function testIntersectionReturnTypeCanNarrowToIntersectionOrConcreteSubtype(): void
{
$this->assertCompiles('return_type_covariance_intersection.php');
}
public function testParameterTypeCannotBeCovariant()
{
$this->exec('must be compatible', 'inheritance_error_param_covariant_class.php');

@ -321,6 +321,25 @@ class PreprocessorTest extends TestCase
$this->assertSame('App\\VERSION', $constants['_const_var_App__VERSION']->name);
}
public function testNamespaceEndingCommentWithUnbracketedSyntaxIsIgnored(): void
{
global $translator;
$file = __DIR__ . '/../code/preprocessor/namespace_ending_comment_unbracketed.php';
$previousTranslator = $translator ?? null;
$translator = $this->compiler;
try {
$this->compiler->addFiles([$file]);
$this->compiler->prepareFile($file);
$this->compiler->convertFile($file);
} finally {
$translator = $previousTranslator;
}
$constants = $this->getProperty('constants');
$this->assertArrayHasKey('_const_var_NamespaceEndingComment__VALUE', $constants);
}
public function testSortFilesUsesImplementsAndTraitDependencies(): void
{
$classFile = realpath(__DIR__ . '/../code/preprocessor/deps_class_implements.php');

@ -3172,11 +3172,17 @@ class CompilerBase implements PropertyAccessContext
$className = $this->getNamespacedClassName($className);
}
$ctorClassName = $className;
if ($this->hasClass($className)) {
$classDef = $this->getClass($className);
if ($classDef->flags & Modifiers::ABSTRACT) {
$this->fatalError($expr, "abstract class `{$className}` cannot be instantiated");
}
if ($this->isAbstractClass($className)) {
$this->fatalError($expr, "abstract class `{$className}` cannot be instantiated");
}
$constructor = $this->findConstructor($className);
if ($constructor !== null
&& !$this->checkAccessibleByClassName($constructor['className'], $constructor['flags'])) {
$this->fatalError(
$expr,
'Cannot call ' . $this->visibilityLabel($constructor['flags']) . ' '
. $constructor['className'] . '::__construct()'
);
}
$cePtr = $this->getClassEntryPtr($className);
}
@ -3833,6 +3839,11 @@ class CompilerBase implements PropertyAccessContext
}
protected function checkAccessible(ClassDef $classDef, int $flags): bool
{
return $this->checkAccessibleByClassName($classDef->getNamespacedName(false), $flags);
}
protected function checkAccessibleByClassName(string $declaringClass, int $flags): bool
{
$scopeClassDef = $this->classDef;
if ($this->functionDef !== null
@ -3843,7 +3854,7 @@ class CompilerBase implements PropertyAccessContext
// 私有方法,只能当前的类使用
if ($flags & Modifiers::PRIVATE) {
return $scopeClassDef !== null
&& strcasecmp($classDef->getNamespacedName(false), $scopeClassDef->getNamespacedName(false)) === 0;
&& $this->isSameClassName($declaringClass, $scopeClassDef->getNamespacedName(false));
}
// 保护方法,只能当前类和子类使用
if ($flags & Modifiers::PROTECTED) {
@ -3852,13 +3863,60 @@ class CompilerBase implements PropertyAccessContext
}
return $this->canAccessProtectedProperty(
$scopeClassDef->getNamespacedName(false),
$classDef->getNamespacedName(false)
$declaringClass
);
}
// 类外部调用,只允许调用 public 方法
return true;
}
/**
* 沿继承链查找实际调用的构造函数,包括项目类继承的内部类构造函数。
*
* @return array{className: string, flags: int}|null
*/
protected function findConstructor(string $className): ?array
{
$current = $className;
while ($current !== '') {
if ($this->hasClass($current)) {
$classDef = $this->getClass($current);
if ($classDef->hasMethod('__construct')) {
return [
'className' => $classDef->getNamespacedName(false),
'flags' => $classDef->getMethod('__construct')->flags,
];
}
$current = $classDef->extends;
continue;
}
if (!$this->isInternalClass($current)) {
return null;
}
$constructor = Reflection::getClass($current)?->getConstructor();
if ($constructor === null) {
return null;
}
return [
'className' => $constructor->getDeclaringClass()->getName(),
'flags' => $constructor->getModifiers(),
];
}
return null;
}
protected function visibilityLabel(int $flags): string
{
if ($flags & Modifiers::PRIVATE) {
return 'private';
}
if ($flags & Modifiers::PROTECTED) {
return 'protected';
}
return 'public';
}
protected function genDebugInfo(?NodeAbstract $stmt = null, string $functionName = '', int $startLine = 0): string
{
$code = '';

@ -33,14 +33,6 @@ class FunctionDef
public string $attributeFactoryScope = '';
/** External library imported by the stub containing this function. */
public string $importLibrary = '';
/**
* True for a trait method whose body contains `parent::` calls. Such methods
* receive an implicit `zend_class_entry *trait_parent_ce` parameter (right after
* `this_`) so the `parent::` call can be bound to the class that composes the
* trait. Both the definition and the shared `func_decl.h` declaration must emit
* this parameter, otherwise the declaration/definition signatures disagree.
*/
public bool $traitParentCe = false;
public bool $returnTypeUndeclared = false;
public bool $returnsByRef = false;
public bool $generator = false;
@ -94,6 +86,7 @@ class FunctionDef
public ?string $declaredReturnType = null;
public string $declaredReturnClass = '';
public ?array $declaredReturnTypeCheck = null;
public string $declaredReturnTypeStr = '';
public function __construct(string $name, string $returnType, string $namespace)
{

@ -83,6 +83,7 @@ trait FiberGenerator
}
$functionDef->declaredReturnType = $functionDef->returnType;
$functionDef->declaredReturnClass = $functionDef->returnClass;
$functionDef->declaredReturnTypeStr = $functionDef->returnTypeStr;
$functionDef->generator = true;
$functionDef->returnType = Type::VAR;
$functionDef->returnClass = '';

@ -116,7 +116,7 @@ trait TypeCheckGenerator
return $class ? [['kind' => 'instanceof', 'class' => $class]] : [];
}
private function typeCheckNodeToString(NodeAbstract $typeNode): string
protected function typeCheckNodeToString(NodeAbstract $typeNode): string
{
if ($typeNode instanceof Node\Identifier) {
return $typeNode->name;

@ -321,6 +321,8 @@ class Preprocessor extends CompilerBase
case 'Stmt_Interface':
$this->parseInterface($v2);
break;
case 'Stmt_Nop':
break;
default:
$this->foundStrayCode($v2);
break;
@ -542,6 +544,9 @@ class Preprocessor extends CompilerBase
}
$functionDef->exported = !($this->classDef?->exported === false || $this->hasNoExportAttribute($v));
$functionDef->returnClass = $class;
$functionDef->returnTypeStr = $v->returnType === null
? ''
: $this->typeCheckNodeToString($v->returnType);
// Record late-bound return type keywords so they can be re-resolved to
// the consuming class when a trait method is flattened into a class.
$functionDef->returnTypeKeyword = $returnTypeKeyword;
@ -560,7 +565,6 @@ class Preprocessor extends CompilerBase
$typeInfo = $this->buildTypeCheckFromNode($v->returnType);
if (!empty($typeInfo['check'])) {
$functionDef->returnTypeCheck = $typeInfo['check'];
$functionDef->returnTypeStr = $typeInfo['typeStr'];
$functionDef->returnTypeNode = $v->returnType;
}
}

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

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

@ -10,6 +10,8 @@ interface GenInterface
interface IterableInterface
{
public function it(array $array): iterable;
public function narrowed(array $array): iterable;
}
interface NullableInterface
@ -38,6 +40,13 @@ class Box implements GenInterface, IterableInterface, NullableInterface, UnionIn
}
}
public function narrowed(array $array): \Generator
{
foreach ($array as $value) {
yield $value;
}
}
public function nullable(array $array): ?\Generator
{
foreach ($array as $value) {
@ -62,6 +71,9 @@ function main()
foreach ($box->it([4, 5]) as $v) {
var_dump($v);
}
foreach ($box->narrowed([10, 11]) as $v) {
var_dump($v);
}
foreach ($box->nullable([6, 7]) as $v) {
var_dump($v);
}
@ -76,6 +88,8 @@ int(4)
int(6)
int(4)
int(5)
int(10)
int(11)
int(6)
int(7)
int(8)

@ -0,0 +1,22 @@
--TEST--
A namespace block ending with a comment must not be treated as stray code
--FILE--
<?php
declare(strict_types=1);
namespace Test {
/* named namespace trailing block comment */
}
namespace {
function main()
{
var_dump('done');
}
// global namespace trailing line comment
}
?>
--EXPECT--
string(4) "done"

@ -0,0 +1,26 @@
--TEST--
Constructor visibility - protected constructor accessible from subclass
--FILE--
<?php
class Base
{
protected function __construct(){}
}
class Sub extends Base
{
public static function make(): Base
{
return new Base();
}
}
function main()
{
$obj = Sub::make();
var_dump($obj instanceof Base);
}
?>
--EXPECT--
bool(true)

@ -0,0 +1,27 @@
--TEST--
Trait __construct is used by the composing class
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
public function __construct()
{
echo "trait ctor\n";
}
}
class TestClass
{
use TestTrait;
}
function main()
{
new TestClass();
}
?>
--EXPECT--
trait ctor

@ -0,0 +1,32 @@
--TEST--
Class __construct overrides the one provided by a trait
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
public function __construct()
{
echo "trait ctor\n";
}
}
class TestClass
{
use TestTrait;
public function __construct()
{
echo "class ctor\n";
}
}
function main()
{
new TestClass();
}
?>
--EXPECT--
class ctor

@ -0,0 +1,37 @@
--TEST--
Trait protected __construct is accessible from a subclass
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
protected function __construct()
{
echo "base ctor\n";
}
}
class BaseClass
{
use TestTrait;
}
class SubClass extends BaseClass
{
public function __construct()
{
new BaseClass();
echo "sub ctor\n";
}
}
function main()
{
new SubClass();
}
?>
--EXPECT--
base ctor
sub ctor

@ -0,0 +1,30 @@
--TEST--
Trait __construct with arguments and $this property access
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
private int $value = 0;
public function __construct(int $value)
{
$this->value = $value;
echo "value=" . $this->value . "\n";
}
}
class TestClass
{
use TestTrait;
}
function main()
{
new TestClass(42);
}
?>
--EXPECT--
value=42

@ -0,0 +1,120 @@
--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();
}
}
class StaticBase
{
public function copy(): ?self
{
return $this;
}
}
class StaticChild extends StaticBase
{
public function copy(): ?static
{
return $this;
}
}
interface IterableContract
{
public function values(): iterable;
}
class IterableImpl implements IterableContract
{
public function values(): array
{
return [1, 2];
}
}
interface BoolContract
{
public function enabled(): bool;
}
class LiteralBoolImpl implements BoolContract
{
public function enabled(): true
{
return true;
}
}
abstract class VoidContract
{
abstract public function stop(): void;
}
abstract class NeverImpl extends VoidContract
{
public function stop(): never
{
throw new RuntimeException('stop');
}
}
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);
$static = new StaticChild();
var_dump($static->copy() instanceof StaticChild);
var_dump((new IterableImpl())->values());
var_dump((new LiteralBoolImpl())->enabled());
}
?>
--EXPECT--
int(42)
bool(true)
bool(true)
bool(true)
array(2) {
[0]=>
int(1)
[1]=>
int(2)
}
bool(true)
Loading…
Cancel
Save