fix(php): validate composite parameter variance

pull/13/head
韩天峰 2 months ago
parent 61099c4c9e
commit 72db18c22a
  1. 13
      phpunit/code/interface_param_union_narrows_mixed.php
  2. 13
      phpunit/code/interface_param_union_narrows_untyped.php
  3. 10
      phpunit/src/InheritanceErrorTest.php
  4. 1
      src/Php/ArgInfo.php
  5. 8
      src/Php/Preprocessor.php
  6. 91
      src/Php/Translator.php
  7. 57
      tests/core/classes/class_implements_param_type.phpt

@ -0,0 +1,13 @@
<?php
interface InterfaceParamUnionNarrowsMixed
{
public function value(mixed $v);
}
class InterfaceParamUnionNarrowsMixedImpl implements InterfaceParamUnionNarrowsMixed
{
public function value(string|int $v)
{
}
}

@ -0,0 +1,13 @@
<?php
interface InterfaceParamUnionNarrowsUntyped
{
public function value($v);
}
class InterfaceParamUnionNarrowsUntypedImpl implements InterfaceParamUnionNarrowsUntyped
{
public function value(string|int $v)
{
}
}

@ -58,6 +58,16 @@ class InheritanceErrorTest extends TestCase
$this->exec('must be compatible', 'inheritance_error_param_covariant_class.php'); $this->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() public function testByRefMismatch()
{ {
$this->exec('must be compatible', 'inheritance_error_byref.php'); $this->exec('must be compatible', 'inheritance_error_byref.php');

@ -25,6 +25,7 @@ class ArgInfo
public bool $variadic = false; public bool $variadic = false;
public bool $nullable = false; public bool $nullable = false;
public bool $undeclared = false; public bool $undeclared = false;
public bool $explicitMixed = false;
public bool $property = false; public bool $property = false;
/** /**

@ -237,6 +237,14 @@ class Preprocessor extends CompilerBase
$class = ''; $class = '';
$type = $this->parseTypeDecl($param->type, self::DECL_TYPE_OF_PARAM, $class); $type = $this->parseTypeDecl($param->type, self::DECL_TYPE_OF_PARAM, $class);
$argInfo->undeclared = $param->type === null; $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)) { if ($class and !$this->hasInterface($class) and !$this->isAbstractClass($class)) {
$argInfo->class = $class; $argInfo->class = $class;
} }

@ -3358,15 +3358,22 @@ CODE;
{ {
// Child methods may omit parameter types (contravariance — accepting a // Child methods may omit parameter types (contravariance — accepting a
// wider set of inputs is always compatible with the parent contract). // 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; return true;
} }
if ($parentArg->undeclared || $parentArg->type === self::TYPE_VAR) { if ($this->isTopParameterType($parentArg)) {
return false; 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) { if ($childArg->type !== $parentArg->type) {
return false; return false;
} }
@ -3382,6 +3389,82 @@ CODE;
return $this->isInheritedFrom($parentArg->class, $childArg->class); 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 private function checkInterfaceImplementations(Node\Stmt\Class_|Node\Stmt\Enum_ $classStmt): void
{ {
$classDef = $this->classDef; $classDef = $this->classDef;

@ -1,15 +1,8 @@
--TEST-- --TEST--
Class implements interface: union type / composite type parameter compatibility Class implements interface: parameter type 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-- --FILE--
<?php <?php
// -------------------------------------------------------
// Interface with single + union type parameters
// -------------------------------------------------------
interface ContractA interface ContractA
{ {
public function single(string $value); public function single(string $value);
@ -18,18 +11,14 @@ interface ContractA
public function nullableUnion(string|int|null $value); public function nullableUnion(string|int|null $value);
} }
// Child omits every parameter type — must be compatible with all.
class ImplOmitAll implements ContractA class ImplOmitAll implements ContractA
{ {
public function single($value) { var_dump($value); } public function single($value) { var_dump($value); }
public function union2($value) { var_dump($value); } public function union2($value) { var_dump($value); }
public function union3($value) { var_dump($value); } public function union3($value) { var_dump($value); }
public function nullableUnion($value) { var_dump($value); } public function nullableUnion($value) { var_dump($value); }
} }
// -------------------------------------------------------
// Interface where child mirrors the union type exactly
// -------------------------------------------------------
interface ContractB interface ContractB
{ {
public function mirror(string|int $x); public function mirror(string|int $x);
@ -43,19 +32,30 @@ class ImplMirror implements ContractB
} }
} }
// -------------------------------------------------------
// Multiple interfaces with different union types
// -------------------------------------------------------
interface ContractC interface ContractC
{ {
public function a(string|bool $v); public function widen(string $v);
}
class ImplWiden implements ContractC
{
public function widen(string|int $v)
{
var_dump($v);
}
} }
interface ContractD interface ContractD
{
public function a(string|bool $v);
}
interface ContractE
{ {
public function b(int|float $v); public function b(int|float $v);
} }
class ImplMulti implements ContractC, ContractD class ImplMulti implements ContractD, ContractE
{ {
public function a($v) { var_dump($v); } public function a($v) { var_dump($v); }
public function b($v) { var_dump($v); } public function b($v) { var_dump($v); }
@ -63,7 +63,6 @@ class ImplMulti implements ContractC, ContractD
function main() function main()
{ {
// ImplOmitAll — omitted types should still pass runtime checks
$a = new ImplOmitAll; $a = new ImplOmitAll;
$a->single('hello'); $a->single('hello');
$a->union2('world'); $a->union2('world');
@ -74,17 +73,18 @@ function main()
$a->nullableUnion(100); $a->nullableUnion(100);
$a->nullableUnion(null); $a->nullableUnion(null);
// ImplMirror — explicit matching union type
$b = new ImplMirror; $b = new ImplMirror;
$b->mirror('ok'); $b->mirror('ok');
$b->mirror(99); $b->mirror(99);
// ImplMulti — multiple interfaces $c = new ImplWiden;
$c = new ImplMulti; $c->widen('wide');
$c->a('yes');
$c->a(true); $d = new ImplMulti;
$c->b(123); $d->a('yes');
$c->b(4.56); $d->a(true);
$d->b(123);
$d->b(4.56);
} }
?> ?>
--EXPECT-- --EXPECT--
@ -98,6 +98,7 @@ int(100)
NULL NULL
string(2) "ok" string(2) "ok"
int(99) int(99)
string(4) "wide"
string(3) "yes" string(3) "yes"
bool(true) bool(true)
int(123) int(123)

Loading…
Cancel
Save