From 61099c4c9e36091441935409ade6d9d146939440 Mon Sep 17 00:00:00 2001 From: Yurun Date: Tue, 7 Jul 2026 11:40:01 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(php):=20=E4=BF=AE=E5=A4=8D=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=B1=BB=E5=9E=8B=E9=87=8D=E5=86=99=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7=E6=A3=80=E6=9F=A5=E4=BB=A5=E9=81=B5=E5=BE=AA=E9=80=86?= =?UTF-8?q?=E5=8F=98=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Php/Translator.php | 8 +- .../classes/class_implements_param_type.phpt | 104 ++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 tests/core/classes/class_implements_param_type.phpt diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 23386942..c736f0a6 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -3356,15 +3356,17 @@ CODE; private function isParameterTypeOverrideCompatible(ArgInfo $childArg, ArgInfo $parentArg): bool { - if ($parentArg->typeCheck || $childArg->typeCheck) { - return $parentArg->typeStr === $childArg->typeStr; - } + // 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) { return true; } if ($parentArg->undeclared || $parentArg->type === self::TYPE_VAR) { return false; } + if ($parentArg->typeCheck || $childArg->typeCheck) { + return $parentArg->typeStr === $childArg->typeStr; + } if ($childArg->type !== $parentArg->type) { return false; } diff --git a/tests/core/classes/class_implements_param_type.phpt b/tests/core/classes/class_implements_param_type.phpt new file mode 100644 index 00000000..55e76b17 --- /dev/null +++ b/tests/core/classes/class_implements_param_type.phpt @@ -0,0 +1,104 @@ +--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) From 72db18c22a6c7cdef05277ea41f7acf9ddcec71e Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 7 Jul 2026 12:18:12 +0800 Subject: [PATCH 2/2] fix(php): validate composite parameter variance --- .../interface_param_union_narrows_mixed.php | 13 ++ .../interface_param_union_narrows_untyped.php | 13 ++ phpunit/src/InheritanceErrorTest.php | 10 + src/Php/ArgInfo.php | 1 + src/Php/Preprocessor.php | 8 + src/Php/Translator.php | 91 +++++++- .../classes/class_implements_param_type.phpt | 209 +++++++++--------- 7 files changed, 237 insertions(+), 108 deletions(-) create mode 100644 phpunit/code/interface_param_union_narrows_mixed.php create mode 100644 phpunit/code/interface_param_union_narrows_untyped.php 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)