Merge branch 'master' into stub-fix-heredoc-nowdoc-illegal-cpp

pull/36/head^2
韩天峰 1 month ago
commit d9b50f556e
  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. 14
      src/Entity/FunctionDef.php
  22. 25
      src/Generator/DefaultArgumentGenerator.php
  23. 18
      src/Generator/FiberGenerator.php
  24. 2
      src/Generator/TypeCheckGenerator.php
  25. 22
      src/Parser/AssignOpTrait.php
  26. 6
      src/Preprocessor.php
  27. 176
      src/Translator.php
  28. 47
      tests/compiler/class/interface-return-covariance.phpt
  29. 30
      tests/compiler/const/class-const-default-value-typed.phpt
  30. 57
      tests/compiler/generator/generator-foreach-yield.phpt
  31. 53
      tests/compiler/generator/generator-return-type-generator.phpt
  32. 96
      tests/compiler/generator/interface-return-type-variants.phpt
  33. 38
      tests/compiler/generator/interface-return-type.phpt
  34. 22
      tests/compiler/namespace/namespace-ending-comment.phpt
  35. 26
      tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt
  36. 29
      tests/compiler/ref/array-ref-assign-001.phpt
  37. 51
      tests/compiler/ref/array-ref-assign-002.phpt
  38. 37
      tests/compiler/ref/array-ref-assign-003.phpt
  39. 27
      tests/compiler/trait/trait-ctor-basic.phpt
  40. 32
      tests/compiler/trait/trait-ctor-override.phpt
  41. 37
      tests/compiler/trait/trait-ctor-protected-subclass.phpt
  42. 30
      tests/compiler/trait/trait-ctor-with-args.phpt
  43. 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 = '';

@ -74,6 +74,20 @@ class FunctionDef
/** Original union/nullable return type AST node. */
public ?NodeAbstract $returnTypeNode = null;
/**
* Source-level return type declared on a generator method, preserved after
* `prepareGeneratorFunction()` neutralizes the runtime return type. A
* generator actually returns a `\FiberGenerator` (which implements
* `Iterator`), so the C++ return type and runtime type check are left
* neutral; this copy is only used by interface/abstract return-type
* covariance checks so a generator method can still satisfy a contract such
* as `: \Generator`.
*/
public ?string $declaredReturnType = null;
public string $declaredReturnClass = '';
public ?array $declaredReturnTypeCheck = null;
public string $declaredReturnTypeStr = '';
public function __construct(string $name, string $returnType, string $namespace)
{
$this->name = $name;

@ -88,7 +88,8 @@ trait DefaultArgumentGenerator
$code .= 'return ' . $plan->expr . ';' . PHP_EOL;
}
} else {
$code .= 'return ' . $argInfo->default . ';' . PHP_EOL;
$default = $this->convertRuntimeConstantDefault($type, $argInfo->default);
$code .= 'return ' . $default . ';' . PHP_EOL;
}
$code .= '}' . PHP_EOL . PHP_EOL;
@ -98,6 +99,28 @@ trait DefaultArgumentGenerator
return $code;
}
/**
* Runtime constant lookup returns Variant, but a typed default helper must
* return its native C++ type explicitly. Convert the complete expression so
* constants nested in expressions are covered as well.
*/
private function convertRuntimeConstantDefault(string $type, string $default): string
{
if (!str_contains($default, 'php::constant(')) {
return $default;
}
return match ($type) {
Type::INT => 'php::toInt(' . $default . ')',
Type::FLOAT => 'php::toFloat(' . $default . ')',
Type::BOOL => 'php::toBool(' . $default . ')',
Type::STR => 'php::toString(' . $default . ')',
Type::ARRAY => 'php::toArray(' . $default . ')',
Type::OBJECT => 'php::toObject(' . $default . ')',
default => $default,
};
}
private function shouldGenerateDefaultArgumentHelper(ArgInfo $argInfo): bool
{
if ($argInfo->variadic) {

@ -72,6 +72,18 @@ trait FiberGenerator
if (!$this->generatorReturnTypeAcceptsFiber($v->returnType)) {
$this->fatalError($v, 'Generator return type must accept \\FiberGenerator; use Iterator, Traversable, iterable, object, mixed, or omit the return type');
}
// Preserve the source-level declared return type before neutralizing the
// runtime return type. The override compatibility check still needs it so
// a generator method can satisfy an interface/abstract contract such as
// `: \Generator` (the runtime object is a `\FiberGenerator`, not a Zend
// `Generator`, so the C++ return type and runtime check stay neutral).
if ($v->returnType !== null) {
$declared = $this->buildTypeCheckFromNode($v->returnType);
$functionDef->declaredReturnTypeCheck = $declared['check'] ?: null;
}
$functionDef->declaredReturnType = $functionDef->returnType;
$functionDef->declaredReturnClass = $functionDef->returnClass;
$functionDef->declaredReturnTypeStr = $functionDef->returnTypeStr;
$functionDef->generator = true;
$functionDef->returnType = Type::VAR;
$functionDef->returnClass = '';
@ -112,7 +124,11 @@ trait FiberGenerator
[, $class] = $this->resolveTypeDecl($type, self::DECL_TYPE_OF_RETURN);
$class = strtolower(ltrim($class, '\\'));
return in_array($class, ['iterator', 'traversable', 'fibergenerator'], true);
// `\Generator` is the return type PHP programmers naturally write for a
// generator. TypePHP generators actually return a `\FiberGenerator`, so
// accepting the declared `Generator` type keeps PHP source compatible
// while the runtime object remains a `\FiberGenerator`.
return in_array($class, ['iterator', 'traversable', 'fibergenerator', 'generator'], true);
}
protected function parseYieldExpr(Yield_ $expr): string

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

@ -42,11 +42,27 @@ trait AssignOpTrait
$tmp = $this->genTmpVarName();
$this->addLocalVar($tmp, Type::VAR);
// item(dim, true) updates an existing reference's value, while offsetSet()
// replaces the array bucket and breaks the reference. Keep offsetSet() for
// ArrayAccess objects; dynamically typed/reference containers need a
// runtime array check because either representation is possible.
$arrayType = $this->getVarType($array);
if ($left->dim === null) {
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet(" . self::VALUE_NULL . ", {$tmp})" . '), ' . $tmp . ')';
}
$dim = $this->parseIdentifier($left->dim);
if ($arrayType === Type::ARRAY) {
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.item({$dim}, true) = {$tmp}" . '), ' . $tmp . ')';
}
if ($arrayType === Type::VAR || $arrayType === Type::REF) {
$writeArray = "static_cast<void>({$array}.item({$dim}, true) = {$tmp})";
$writeOther = "{$array}.offsetSet({$dim}, {$tmp})";
return $code . '((' . $tmp . ' = ' . $value . ', '
. "({$array}.isArray() ? {$writeArray} : {$writeOther})"
. '), ' . $tmp . ')';
}
return $code . '((' . $tmp . ' = ' . $value . ', ' . "{$array}.offsetSet({$dim}, {$tmp})" . '), ' . $tmp . ')';
}
@ -746,6 +762,9 @@ trait AssignOpTrait
}
$left = $this->parseWritableIdentifier($expr->var);
// Keep this write-context form for every RHS kind. Re-parsing it as a
// read later breaks append and missing-key targets such as
// `$array[] =& $source`.
if ($this->isVarExpr($expr->var)) {
if (!$this->hasVar($left)) {
@ -822,13 +841,10 @@ trait AssignOpTrait
}
}
} elseif ($this->isPropertyFetch($expr->expr)) {
$left = $this->parseIdentifier($expr->var);
$rightExpr = $tmpVar . ' = ' . $this->emitDynamicPropertyFetchRef($expr->expr, $expr);
} elseif ($this->isStaticPropertyFetch($expr->expr)) {
$left = $this->parseIdentifier($expr->var);
$rightExpr = $tmpVar . ' = ' . $this->emitStaticPropertyFetchRef($expr->expr, $expr);
} elseif ($this->isArrayDimFetch($expr->expr)) {
$left = $this->parseIdentifier($expr->var);
$array = $this->parseWritableIdentifier($expr->expr->var);
if ($expr->expr->dim == null) {
$this->fatalError($expr, 'Cannot assign reference to array dim fetch without dim');

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

@ -2514,6 +2514,8 @@ CODE;
case 'Stmt_Interface':
$this->validateInterfaceOverrideAttributes($v2);
break;
case 'Stmt_Nop':
break;
default:
abort($v2);
break;
@ -3624,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) {
@ -3709,33 +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;
}
if ($parentFuncDef->returnTypeCheck || $childFuncDef->returnTypeCheck) {
return $parentFuncDef->returnTypeStr === $childFuncDef->returnTypeStr;
$parentTypes = $this->getReturnAcceptedTypes($parentFuncDef, $parentClass);
$childTypes = $this->getReturnAcceptedTypes($childFuncDef, $childClass);
// 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;
}
}
if ($parentFuncDef->returnType === Type::VAR) {
return true;
return true;
}
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 ($childFuncDef->returnType !== $parentFuncDef->returnType) {
return false;
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']],
},
};
}
private function normalizeReturnTypeEntry(array $type, string $declaringClass): array
{
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 $type;
}
private function isReturnTypeCoveredBy(array $childType, array $parentTypes): bool
{
$childClause = ($childType['kind'] ?? null) === 'allOf'
? $childType['types']
: [$childType];
foreach ($parentTypes as $parentType) {
$parentClause = ($parentType['kind'] ?? null) === 'allOf'
? $parentType['types']
: [$parentType];
if ($this->isReturnTypeClauseSubtype($childClause, $parentClause)) {
return true;
}
}
return false;
}
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;
}
}
if (!$covered) {
return false;
}
}
return true;
}
private function isReturnTypeEntryCompatible(array $childType, array $parentType): bool
{
$childKind = $childType['kind'] ?? null;
$parentKind = $parentType['kind'] ?? null;
if ($childKind === 'isNever' || $parentKind === 'isMixed') {
return true;
}
if ($parentFuncDef->returnType !== Type::OBJECT) {
if (($childKind === 'isTrue' || $childKind === 'isFalse') && $parentKind === 'isBool') {
return true;
}
if ($childFuncDef->returnClass === $parentFuncDef->returnClass) {
if ($childKind === 'isArray' && $parentKind === 'iterable') {
return true;
}
if (!$childFuncDef->returnClass || !$parentFuncDef->returnClass) {
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') {
$parentClass = $parentType['class'] ?? '';
return $childClass !== ''
&& $parentClass !== ''
&& $this->isInheritedFrom($childClass, $parentClass);
}
return false;
}
return $this->isInheritedFrom($childFuncDef->returnClass, $parentFuncDef->returnClass);
return $childKind !== null && $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,30 @@
--TEST--
Typed parameter default value from an unresolvable (external) class constant
--FILE--
<?php
class TypedDefault
{
public function run(
int $value = \ArrayObject::ARRAY_AS_PROPS,
float $floatValue = \ArrayObject::ARRAY_AS_PROPS,
string $format = \DateTime::ATOM,
int $composite = 1 | \ArrayObject::ARRAY_AS_PROPS,
mixed $variant = \ArrayObject::STD_PROP_LIST,
)
{
var_dump($value, $floatValue, $format, $composite, $variant);
}
}
function main()
{
(new TypedDefault)->run();
}
?>
--EXPECT--
int(2)
float(2)
string(13) "Y-m-d\TH:i:sP"
int(3)
int(1)

@ -0,0 +1,57 @@
--TEST--
generator re-yielding array elements via foreach with \Generator return type
--FILE--
<?php
function main()
{
$g = test([1, 2, 3]);
var_dump($g);
foreach ($g as $value)
{
var_dump($value);
}
}
function test(array $array): \Generator
{
foreach ($array as $value)
{
yield $value;
}
}
// main();
?>
--EXPECTF--
object(FiberGenerator)#%d (9) {
["callback":"FiberGenerator":private]=>
object(Closure)#%d (2) {
["function"]=>
string(19) "stdClass::{closure}"
["this"]=>
object(stdClass)#%d (1) {
["box"]=>
resource(%d) of type (php::box)
}
}
["fiber":"FiberGenerator":private]=>
NULL
["current":"FiberGenerator":private]=>
NULL
["key":"FiberGenerator":private]=>
NULL
["valid":"FiberGenerator":private]=>
bool(false)
["state":"FiberGenerator":private]=>
int(0)
["yield_count":"FiberGenerator":private]=>
int(0)
["next_index":"FiberGenerator":private]=>
int(0)
["return_value":"FiberGenerator":private]=>
NULL
}
int(1)
int(2)
int(3)

@ -0,0 +1,53 @@
--TEST--
generator return type accepts \Generator for methods, nullable and union variants
--FILE--
<?php
class Box
{
public function gen(array $array): \Generator
{
foreach ($array as $value) {
yield $value * 2;
}
}
}
function nullableGen(array $array): ?\Generator
{
foreach ($array as $value) {
yield $value;
}
}
function unionGen(array $array): \Generator|\Iterator
{
foreach ($array as $value) {
yield $value;
}
}
function main()
{
$b = new Box();
foreach ($b->gen([1, 2, 3]) as $v) {
var_dump($v);
}
$g = nullableGen([4, 5]);
foreach ($g as $v) {
var_dump($v);
}
$u = unionGen([6, 7]);
foreach ($u as $v) {
var_dump($v);
}
}
?>
--EXPECT--
int(2)
int(4)
int(6)
int(4)
int(5)
int(6)
int(7)

@ -0,0 +1,96 @@
--TEST--
generator methods implementing interfaces with iterable, nullable and union return types
--FILE--
<?php
interface GenInterface
{
public function gen(array $array): \Generator;
}
interface IterableInterface
{
public function it(array $array): iterable;
public function narrowed(array $array): iterable;
}
interface NullableInterface
{
public function nullable(array $array): ?\Generator;
}
interface UnionInterface
{
public function union(array $array): \Generator|\Iterator;
}
class Box implements GenInterface, IterableInterface, NullableInterface, UnionInterface
{
public function gen(array $array): \Generator
{
foreach ($array as $value) {
yield $value * 2;
}
}
public function it(array $array): iterable
{
foreach ($array as $value) {
yield $value;
}
}
public function narrowed(array $array): \Generator
{
foreach ($array as $value) {
yield $value;
}
}
public function nullable(array $array): ?\Generator
{
foreach ($array as $value) {
yield $value;
}
}
public function union(array $array): \Generator|\Iterator
{
foreach ($array as $value) {
yield $value;
}
}
}
function main()
{
$box = new Box();
foreach ($box->gen([1, 2, 3]) as $v) {
var_dump($v);
}
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);
}
foreach ($box->union([8, 9]) as $v) {
var_dump($v);
}
}
?>
--EXPECT--
int(2)
int(4)
int(6)
int(4)
int(5)
int(10)
int(11)
int(6)
int(7)
int(8)
int(9)

@ -0,0 +1,38 @@
--TEST--
generator method implementing an interface that declares \Generator return type
--FILE--
<?php
interface T
{
public function test(array $array): \Generator;
}
class TestClass implements T
{
public function test(array $array): \Generator
{
foreach ($array as $value) {
yield $value;
}
}
}
function main()
{
$test = new TestClass;
$g = $test->test([1, 2, 3]);
// TypePHP generators return a \FiberGenerator which implements Iterator
// but is NOT the Zend \Generator class.
var_dump($g instanceof \Generator);
var_dump($g instanceof \Iterator);
foreach ($g as $value) {
var_dump($value);
}
}
?>
--EXPECT--
bool(false)
bool(true)
int(1)
int(2)
int(3)

@ -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,29 @@
--TEST--
array reference assignment: append and element assignment write back through reference
--FILE--
<?php
function main()
{
$arr1 = [1, 2, 3];
$arr2 = [&$arr1[0]];
$arr2[0] = 123;
$arr2[] = &$arr1[1];
$arr2[1] = 456;
var_dump($arr1, $arr2);
}
?>
--EXPECT--
array(3) {
[0]=>
&int(123)
[1]=>
&int(456)
[2]=>
int(3)
}
array(2) {
[0]=>
&int(123)
[1]=>
&int(456)
}

@ -0,0 +1,51 @@
--TEST--
array reference assignment to element: $arr[$k] = &$v writes back through reference
--FILE--
<?php
class RefSource
{
public $value = 30;
public static $staticValue = 40;
}
function main()
{
$x = 10;
$y = 20;
$arr = [1, 2, 3];
$arr[0] = &$x; // 覆盖已有元素为引用
$arr[5] = &$y; // 新建元素为引用
$x = 100;
$y = 200;
var_dump($arr[0], $arr[5]); // 100, 200
// 通过元素引用写回
$arr[0] = 111;
$arr[5] = 222;
var_dump($x, $y); // 111, 222
// 嵌套:引用赋值到多维数组元素
$z = 7;
$m = [[1], [2]];
$m[0][0] = &$z;
$z = 77;
var_dump($m[0][0]); // 77
// 左侧数组追加/元素写入不可因右侧是属性引用而被重新按读取解析
$source = new RefSource();
$propertyRefs = [];
$propertyRefs[] = &$source->value;
$propertyRefs[2] = &RefSource::$staticValue;
$propertyRefs[0] = 333;
$propertyRefs[2] = 444;
var_dump($source->value, RefSource::$staticValue);
}
?>
--EXPECT--
int(100)
int(200)
int(111)
int(222)
int(77)
int(333)
int(444)

@ -0,0 +1,37 @@
--TEST--
dynamically typed array element assignment preserves references and ArrayAccess writes
--FILE--
<?php
function writeElement(mixed $container, mixed $key, mixed $value): void
{
$container[$key] = $value;
}
function writeReferencedContainer(mixed &$container, mixed $value): void
{
$container[0] = $value;
}
function main()
{
$referenced = 10;
$array = [&$referenced];
writeElement($array, 0, 123);
var_dump($referenced, $array[0]);
$referencedAgain = 20;
$arrayByReference = [&$referencedAgain];
writeReferencedContainer($arrayByReference, 234);
var_dump($referencedAgain, $arrayByReference[0]);
$object = new ArrayObject();
writeElement($object, 'key', 456);
var_dump($object['key']);
}
?>
--EXPECT--
int(123)
int(123)
int(234)
int(234)
int(456)

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