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. 209
      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');
}
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');

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

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

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

@ -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--
<?php
// -------------------------------------------------------
// Interface with single + union type parameters
// -------------------------------------------------------
interface ContractA
{
public function single(string $value);
public function union2(string|int $value);
public function union3(string|int|float $value);
public function nullableUnion(string|int|null $value);
}
// Child omits every parameter type — must be compatible with all.
class ImplOmitAll implements ContractA
{
public function single($value) { var_dump($value); }
public function union2($value) { var_dump($value); }
public function union3($value) { var_dump($value); }
public function nullableUnion($value) { var_dump($value); }
}
// -------------------------------------------------------
// Interface where child mirrors the union type exactly
// -------------------------------------------------------
interface ContractB
{
public function mirror(string|int $x);
}
class ImplMirror implements ContractB
{
public function mirror(string|int $x)
{
var_dump($x);
}
}
// -------------------------------------------------------
// Multiple interfaces with different union types
// -------------------------------------------------------
interface ContractC
{
public function a(string|bool $v);
}
interface ContractD
{
public function b(int|float $v);
}
class ImplMulti implements ContractC, ContractD
{
public function a($v) { var_dump($v); }
public function b($v) { var_dump($v); }
}
function main()
{
// ImplOmitAll — omitted types should still pass runtime checks
$a = new ImplOmitAll;
$a->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--
<?php
interface ContractA
{
public function single(string $value);
public function union2(string|int $value);
public function union3(string|int|float $value);
public function nullableUnion(string|int|null $value);
}
class ImplOmitAll implements ContractA
{
public function single($value) { var_dump($value); }
public function union2($value) { var_dump($value); }
public function union3($value) { var_dump($value); }
public function nullableUnion($value) { var_dump($value); }
}
interface ContractB
{
public function mirror(string|int $x);
}
class ImplMirror implements ContractB
{
public function mirror(string|int $x)
{
var_dump($x);
}
}
interface ContractC
{
public function widen(string $v);
}
class ImplWiden implements ContractC
{
public function widen(string|int $v)
{
var_dump($v);
}
}
interface ContractD
{
public function a(string|bool $v);
}
interface ContractE
{
public function b(int|float $v);
}
class ImplMulti implements ContractD, ContractE
{
public function a($v) { var_dump($v); }
public function b($v) { var_dump($v); }
}
function main()
{
$a = new ImplOmitAll;
$a->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)

Loading…
Cancel
Save