diff --git a/phpunit/code/interface_param_union_narrows_mixed.php b/phpunit/code/interface_param_union_narrows_mixed.php new file mode 100644 index 00000000..b5690f1c --- /dev/null +++ b/phpunit/code/interface_param_union_narrows_mixed.php @@ -0,0 +1,13 @@ +exec('must be compatible', 'inheritance_error_param_covariant_class.php'); } + public function testUnionParameterCannotNarrowUntypedParent() + { + $this->exec('must be compatible', 'interface_param_union_narrows_untyped.php'); + } + + public function testUnionParameterCannotNarrowMixedParent() + { + $this->exec('must be compatible', 'interface_param_union_narrows_mixed.php'); + } + public function testByRefMismatch() { $this->exec('must be compatible', 'inheritance_error_byref.php'); diff --git a/src/Php/ArgInfo.php b/src/Php/ArgInfo.php index 93843742..03d528b3 100644 --- a/src/Php/ArgInfo.php +++ b/src/Php/ArgInfo.php @@ -25,6 +25,7 @@ class ArgInfo public bool $variadic = false; public bool $nullable = false; public bool $undeclared = false; + public bool $explicitMixed = false; public bool $property = false; /** diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index d9efe66b..ddd37312 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -237,6 +237,14 @@ class Preprocessor extends CompilerBase $class = ''; $type = $this->parseTypeDecl($param->type, self::DECL_TYPE_OF_PARAM, $class); $argInfo->undeclared = $param->type === null; + if ( + $param->type !== null + && !$param->type instanceof NullableType + && !$param->type instanceof UnionType + && !$param->type instanceof IntersectionType + ) { + $argInfo->explicitMixed = in_array(strtolower($this->parseIdentifier($param->type)), ['mixed', 'any'], true); + } if ($class and !$this->hasInterface($class) and !$this->isAbstractClass($class)) { $argInfo->class = $class; } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index c736f0a6..99e87ed9 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -3358,15 +3358,22 @@ CODE; { // Child methods may omit parameter types (contravariance — accepting a // wider set of inputs is always compatible with the parent contract). - if ($childArg->undeclared || $childArg->type === self::TYPE_VAR) { + if ($this->isTopParameterType($childArg)) { return true; } - if ($parentArg->undeclared || $parentArg->type === self::TYPE_VAR) { + if ($this->isTopParameterType($parentArg)) { return false; } - if ($parentArg->typeCheck || $childArg->typeCheck) { - return $parentArg->typeStr === $childArg->typeStr; + + $parentAcceptedTypes = $this->getParameterAcceptedTypes($parentArg); + $childAcceptedTypes = $this->getParameterAcceptedTypes($childArg); + if ($parentAcceptedTypes !== null || $childAcceptedTypes !== null) { + if ($parentAcceptedTypes === null || $childAcceptedTypes === null) { + return false; + } + return $this->isAcceptedTypeSubset($parentAcceptedTypes, $childAcceptedTypes); } + if ($childArg->type !== $parentArg->type) { return false; } @@ -3382,6 +3389,82 @@ CODE; return $this->isInheritedFrom($parentArg->class, $childArg->class); } + private function isTopParameterType(ArgInfo $arg): bool + { + return $arg->undeclared || $arg->explicitMixed; + } + + private function getParameterAcceptedTypes(ArgInfo $arg): ?array + { + if (!empty($arg->typeCheck)) { + return $arg->typeCheck; + } + + return match ($arg->type) { + self::TYPE_INT => [['kind' => 'isInt']], + self::TYPE_FLOAT => [['kind' => 'isFloat']], + self::TYPE_BOOL => [['kind' => 'isBool']], + self::TYPE_STR => [['kind' => 'isString']], + self::TYPE_ARRAY => [['kind' => 'isArray']], + self::TYPE_RESOURCE => [['kind' => 'isResource']], + self::TYPE_OBJECT => $arg->class + ? [['kind' => 'instanceof', 'class' => $arg->class]] + : [['kind' => 'isObject']], + default => null, + }; + } + + private function isAcceptedTypeSubset(array $parentTypes, array $childTypes): bool + { + foreach ($parentTypes as $parentType) { + if (!$this->isAcceptedTypeCovered($parentType, $childTypes)) { + return false; + } + } + return true; + } + + private function isAcceptedTypeCovered(array $parentType, array $childTypes): bool + { + foreach ($childTypes as $childType) { + if ($this->isAcceptedTypeCompatible($parentType, $childType)) { + return true; + } + } + return false; + } + + private function isAcceptedTypeCompatible(array $parentType, array $childType): bool + { + $parentKind = $parentType['kind'] ?? null; + $childKind = $childType['kind'] ?? null; + + if ($parentKind === 'instanceof' && $childKind === 'isObject') { + return true; + } + if ($parentKind !== $childKind) { + return false; + } + + if ($parentKind === 'allOf') { + return $parentType == $childType; + } + + if ($parentKind !== 'instanceof') { + return true; + } + + $parentClass = $parentType['class'] ?? ''; + $childClass = $childType['class'] ?? ''; + if ($parentClass === $childClass) { + return true; + } + if ($parentClass === '' || $childClass === '') { + return false; + } + return $this->isInheritedFrom($parentClass, $childClass); + } + private function checkInterfaceImplementations(Node\Stmt\Class_|Node\Stmt\Enum_ $classStmt): void { $classDef = $this->classDef; diff --git a/tests/core/classes/class_implements_param_type.phpt b/tests/core/classes/class_implements_param_type.phpt index 55e76b17..07c2f4aa 100644 --- a/tests/core/classes/class_implements_param_type.phpt +++ b/tests/core/classes/class_implements_param_type.phpt @@ -1,104 +1,105 @@ ---TEST-- -Class implements interface: union type / composite type parameter compatibility -Tests parameter type contravariance for interface implementation with union types: - - Child method omitting the type should accept a wider set of inputs. - - Child method declaring the identical union type should also compile. - - nullable union (T1|T2|null) and three-way union (T1|T2|T3) are covered. ---FILE-- -single('hello'); - $a->union2('world'); - $a->union2(42); - $a->union3(1); - $a->union3('two'); - $a->union3(3.14); - $a->nullableUnion(100); - $a->nullableUnion(null); - - // ImplMirror — explicit matching union type - $b = new ImplMirror; - $b->mirror('ok'); - $b->mirror(99); - - // ImplMulti — multiple interfaces - $c = new ImplMulti; - $c->a('yes'); - $c->a(true); - $c->b(123); - $c->b(4.56); -} -?> ---EXPECT-- -string(5) "hello" -string(5) "world" -int(42) -int(1) -string(3) "two" -float(3.14) -int(100) -NULL -string(2) "ok" -int(99) -string(3) "yes" -bool(true) -int(123) -float(4.56) +--TEST-- +Class implements interface: parameter type compatibility +--FILE-- +single('hello'); + $a->union2('world'); + $a->union2(42); + $a->union3(1); + $a->union3('two'); + $a->union3(3.14); + $a->nullableUnion(100); + $a->nullableUnion(null); + + $b = new ImplMirror; + $b->mirror('ok'); + $b->mirror(99); + + $c = new ImplWiden; + $c->widen('wide'); + + $d = new ImplMulti; + $d->a('yes'); + $d->a(true); + $d->b(123); + $d->b(4.56); +} +?> +--EXPECT-- +string(5) "hello" +string(5) "world" +int(42) +int(1) +string(3) "two" +float(3.14) +int(100) +NULL +string(2) "ok" +int(99) +string(4) "wide" +string(3) "yes" +bool(true) +int(123) +float(4.56)