diff --git a/phpunit/code/constructor_visibility_inherited_private.php b/phpunit/code/constructor_visibility_inherited_private.php new file mode 100644 index 00000000..53ace63a --- /dev/null +++ b/phpunit/code/constructor_visibility_inherited_private.php @@ -0,0 +1,17 @@ +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'); + } +} diff --git a/phpunit/src/InheritanceErrorTest.php b/phpunit/src/InheritanceErrorTest.php index 59c07e14..0190376c 100644 --- a/phpunit/src/InheritanceErrorTest.php +++ b/phpunit/src/InheritanceErrorTest.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'); diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php index 8e7eeee0..86984f93 100644 --- a/phpunit/src/PreprocessorTest.php +++ b/phpunit/src/PreprocessorTest.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'); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index c183b5a9..5a2f709a 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.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 = ''; diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index 8cf00783..028b69f9 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -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) { diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index e547372b..9aa44dbd 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -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 = ''; diff --git a/src/Generator/TypeCheckGenerator.php b/src/Generator/TypeCheckGenerator.php index 292967a1..66dbb2e2 100644 --- a/src/Generator/TypeCheckGenerator.php +++ b/src/Generator/TypeCheckGenerator.php @@ -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; diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 2d311e00..877cc4f2 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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; } } diff --git a/src/Translator.php b/src/Translator.php index f1f3e55f..66568139 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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); diff --git a/tests/compiler/class/interface-return-covariance.phpt b/tests/compiler/class/interface-return-covariance.phpt new file mode 100644 index 00000000..792c3c76 --- /dev/null +++ b/tests/compiler/class/interface-return-covariance.phpt @@ -0,0 +1,47 @@ +--TEST-- +Interface return type covariance with nullable interface and anonymous class +--FILE-- +test(); + var_dump($result instanceof TestInterface1); + var_dump($result instanceof TestInterface2); + var_dump($result === null); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) diff --git a/tests/compiler/generator/interface-return-type-variants.phpt b/tests/compiler/generator/interface-return-type-variants.phpt index c6ee5ef5..d36b61e0 100644 --- a/tests/compiler/generator/interface-return-type-variants.phpt +++ b/tests/compiler/generator/interface-return-type-variants.phpt @@ -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) diff --git a/tests/compiler/namespace/namespace-ending-comment.phpt b/tests/compiler/namespace/namespace-ending-comment.phpt new file mode 100644 index 00000000..e937a711 --- /dev/null +++ b/tests/compiler/namespace/namespace-ending-comment.phpt @@ -0,0 +1,22 @@ +--TEST-- +A namespace block ending with a comment must not be treated as stray code +--FILE-- + +--EXPECT-- +string(4) "done" diff --git a/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt b/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt new file mode 100644 index 00000000..a1db317b --- /dev/null +++ b/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt @@ -0,0 +1,26 @@ +--TEST-- +Constructor visibility - protected constructor accessible from subclass +--FILE-- + +--EXPECT-- +bool(true) diff --git a/tests/compiler/trait/trait-ctor-basic.phpt b/tests/compiler/trait/trait-ctor-basic.phpt new file mode 100644 index 00000000..1cf76b71 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-basic.phpt @@ -0,0 +1,27 @@ +--TEST-- +Trait __construct is used by the composing class +--FILE-- + +--EXPECT-- +trait ctor diff --git a/tests/compiler/trait/trait-ctor-override.phpt b/tests/compiler/trait/trait-ctor-override.phpt new file mode 100644 index 00000000..5b40f487 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-override.phpt @@ -0,0 +1,32 @@ +--TEST-- +Class __construct overrides the one provided by a trait +--FILE-- + +--EXPECT-- +class ctor diff --git a/tests/compiler/trait/trait-ctor-protected-subclass.phpt b/tests/compiler/trait/trait-ctor-protected-subclass.phpt new file mode 100644 index 00000000..319fd2f3 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-protected-subclass.phpt @@ -0,0 +1,37 @@ +--TEST-- +Trait protected __construct is accessible from a subclass +--FILE-- + +--EXPECT-- +base ctor +sub ctor diff --git a/tests/compiler/trait/trait-ctor-with-args.phpt b/tests/compiler/trait/trait-ctor-with-args.phpt new file mode 100644 index 00000000..5177cb28 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-with-args.phpt @@ -0,0 +1,30 @@ +--TEST-- +Trait __construct with arguments and $this property access +--FILE-- +value = $value; + echo "value=" . $this->value . "\n"; + } +} + +class TestClass +{ + use TestTrait; +} + +function main() +{ + new TestClass(42); +} +?> +--EXPECT-- +value=42 diff --git a/tests/compiler/type_decl/return-type-covariance.phpt b/tests/compiler/type_decl/return-type-covariance.phpt new file mode 100644 index 00000000..68883ac3 --- /dev/null +++ b/tests/compiler/type_decl/return-type-covariance.phpt @@ -0,0 +1,120 @@ +--TEST-- +Return type covariance: union narrowing and object subtype +--FILE-- + 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)