fix(compiler): resolve class and interface dependency registration issues

- Update ReturnInfo::equalsApartFromPhpDocAndRefcount to consider PHPDoc types
  in arginfo equivalence checks to prevent incorrect Reflection type exposure
- Rename method from equalsApartFromPhpDocAndRefcount to equalsApartFromRefcount
  for better clarity and consistency
- Modify Translator.php to use hasClass and hasInterface methods instead of
  symbols->hasClass and symbols->hasInterface for proper dependency resolution
- Add comprehensive tests for class registration preserving reverse inheritance
- Add tests for magic methods with implicit return types maintaining distinct
  Zend arginfo during registration
- Include tests for interface registration handling child-before-parent scenarios
- Add cross-namespace interface dependency resolution tests
- Create diamond inheritance pattern registration preservation tests
pull/40/head
韩天峰 1 month ago
parent 2d4a1b8bb0
commit 0b1a45660e
  1. 4
      src/Translator.php
  2. 14
      src/gen_stub.php
  3. 39
      tests/compiler/class/class-registration-reverse-inheritance.phpt
  4. 35
      tests/compiler/class/interface-registration-child-before-parent.phpt
  5. 35
      tests/compiler/class/interface-registration-cross-namespace.phpt
  6. 39
      tests/compiler/class/interface-registration-diamond.phpt
  7. 76
      tests/compiler/magic_methods/implicit-return-arginfo.phpt
  8. 54
      tests/compiler/magic_methods/implicit-serialization-arginfo.phpt

@ -2423,7 +2423,7 @@ CODE;
if ($parent) {
// 不存在的父类,说明可能是内置类
$tmpCe = $this->getParentClassCe($classDef);
if (!$this->symbols->hasClass($parent)) {
if (!$this->hasClass($parent)) {
$sorter->add($tmpCe);
}
$deps[] = $tmpCe;
@ -2433,7 +2433,7 @@ CODE;
if ($implements) {
foreach ($implements as $interface) {
$tmpCe = self::PREFIX . 'class_entry_' . $this->escapeCeName($interface);
if (!$this->symbols->hasInterface($interface)) {
if (!$this->hasInterface($interface)) {
$sorter->add($tmpCe);
}
$deps[] = $tmpCe;

@ -1155,9 +1155,17 @@ class ReturnInfo {
$this->setRefcount($refcount);
}
public function equalsApartFromPhpDocAndRefcount(ReturnInfo $other): bool {
public function equalsApartFromRefcount(ReturnInfo $other): bool {
// PHPDoc types are used as the effective arginfo type for generated
// TypePHP stubs. They therefore participate in arginfo equivalence:
// aliasing two functions that only differ in their inferred return
// type can expose the wrong Reflection type and may make Zend reject a
// magic method during class registration.
$effectiveType = $this->type ?? $this->phpDocType;
$otherEffectiveType = $other->type ?? $other->phpDocType;
return $this->byRef === $other->byRef
&& StubType::equals($this->type, $other->type)
&& StubType::equals($effectiveType, $otherEffectiveType)
&& $this->tentativeReturnType === $other->tentativeReturnType;
}
@ -1501,7 +1509,7 @@ class FuncInfo {
}
}
return $this->return->equalsApartFromPhpDocAndRefcount($other->return)
return $this->return->equalsApartFromRefcount($other->return)
&& $this->numRequiredArgs === $other->numRequiredArgs
&& $this->cond === $other->cond;
}

@ -0,0 +1,39 @@
--TEST--
class entry registration preserves reverse-declared class and interface dependencies
--FILE--
<?php
interface RegistrationMarker
{
}
class RegistrationLeaf extends RegistrationMiddle implements RegistrationMarker
{
}
class RegistrationMiddle extends RegistrationRoot
{
}
class RegistrationRoot
{
public function name(): string
{
return 'root';
}
}
function main(): void
{
$value = new RegistrationLeaf();
var_dump($value instanceof RegistrationMiddle);
var_dump($value instanceof RegistrationRoot);
var_dump($value instanceof RegistrationMarker);
echo $value->name(), "\n";
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
root

@ -0,0 +1,35 @@
--TEST--
class entry registration preserves a child interface dependency declared before its parent
--FILE--
<?php
namespace CacheContracts {
interface InvalidArgumentException extends CacheException
{
}
interface CacheException extends \Throwable
{
}
class ConcreteException extends \InvalidArgumentException implements InvalidArgumentException
{
}
}
namespace {
function main(): void
{
$exception = new CacheContracts\ConcreteException('invalid cache key');
var_dump($exception instanceof CacheContracts\InvalidArgumentException);
var_dump($exception instanceof CacheContracts\CacheException);
var_dump($exception instanceof Throwable);
echo $exception->getMessage(), "\n";
}
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
invalid cache key

@ -0,0 +1,35 @@
--TEST--
class entry registration resolves cross-namespace interface dependencies before implementors
--FILE--
<?php
namespace RegistrationConsumer {
use RegistrationContracts\BaseContract;
interface ChildContract extends BaseContract
{
}
class Implementation implements ChildContract
{
}
}
namespace RegistrationContracts {
interface BaseContract
{
}
}
namespace {
function main(): void
{
$value = new RegistrationConsumer\Implementation();
var_dump($value instanceof RegistrationConsumer\ChildContract);
var_dump($value instanceof RegistrationContracts\BaseContract);
}
}
?>
--EXPECT--
bool(true)
bool(true)

@ -0,0 +1,39 @@
--TEST--
class entry registration preserves all dependencies in a reverse-declared interface diamond
--FILE--
<?php
interface DiamondLeaf extends DiamondLeft, DiamondRight
{
}
interface DiamondRight extends DiamondRoot
{
}
interface DiamondLeft extends DiamondRoot
{
}
interface DiamondRoot
{
}
class DiamondImplementation implements DiamondLeaf
{
}
function main(): void
{
$value = new DiamondImplementation();
var_dump($value instanceof DiamondLeaf);
var_dump($value instanceof DiamondLeft);
var_dump($value instanceof DiamondRight);
var_dump($value instanceof DiamondRoot);
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)

@ -0,0 +1,76 @@
--TEST--
Magic methods with implicit return types keep distinct Zend arginfo
--FILE--
<?php
class ImplicitMagicReturnTypes
{
private array $values = ['answer' => 42];
public function render()
{
return 'rendered';
}
public function __toString()
{
return $this->render();
}
/**
* @return bool
*/
public function __isset($name)
{
return isset($this->values[$name]);
}
/**
* @return void
*/
public function __unset($name)
{
unset($this->values[$name]);
}
public function __set($name, $value)
{
$this->values[$name] = $value;
}
public function __serialize()
{
return $this->values;
}
public function __unserialize($data)
{
$this->values = $data;
}
}
function returnType(string $method): string
{
$type = (new ReflectionMethod(ImplicitMagicReturnTypes::class, $method))->getReturnType();
return $type ? (string) $type : 'none';
}
function main(): void
{
$value = new ImplicitMagicReturnTypes();
var_dump((string) $value);
foreach (['__toString', '__isset', '__unset', '__set', '__serialize', '__unserialize'] as $method) {
echo $method, ': ', returnType($method), PHP_EOL;
}
}
?>
--EXPECT--
string(8) "rendered"
__toString: string
__isset: bool
__unset: void
__set: void
__serialize: array
__unserialize: void

@ -0,0 +1,54 @@
--TEST--
Implicit serialization magic method return types are not aliased by arginfo deduplication
--FILE--
<?php
class ImplicitSerializationReturnTypes
{
public function noReturnType()
{
return null;
}
public function __serialize()
{
return ['value' => 7];
}
public function __unserialize($data)
{
}
public function __sleep()
{
return [];
}
public function __wakeup()
{
}
}
function serializationReturnType(string $method): string
{
$type = (new ReflectionMethod(ImplicitSerializationReturnTypes::class, $method))->getReturnType();
return $type ? (string) $type : 'none';
}
function main(): void
{
foreach (['noReturnType', '__serialize', '__unserialize', '__sleep', '__wakeup'] as $method) {
echo $method, ': ', serializationReturnType($method), PHP_EOL;
}
$serialized = serialize(new ImplicitSerializationReturnTypes());
var_dump(str_contains($serialized, 'value'));
}
?>
--EXPECT--
noReturnType: none
__serialize: array
__unserialize: void
__sleep: array
__wakeup: void
bool(true)
Loading…
Cancel
Save