fix(translator): allow overrides to add a by-ref return (#53) --skip-tests

* test(phpunit): anchor TypePhp autoloading to the current checkout

A git worktree shares vendor/ with the primary checkout via a symlink, and
Composer's generated autoloader resolves the TypePhp\ prefix relative to the
realpath of vendor/. The suite then silently loads and tests the OTHER
checkout's src/ tree. Prepend an autoloader anchored to this checkout so the
tests always exercise the sources they ship with; in a standalone checkout
this is a no-op.

* fix(translator): allow overrides to add a by-ref return

Zend's inheritance check treats return-by-reference as covariant
(zend_do_perform_implementation_check): an error is raised only when the
parent returns by reference and the child does not. The child adding `&`
is a strictly stronger guarantee and is accepted:

  class A { public function f(): array {} }
  class B extends A { public function &f(): array {} }  // OK in Zend

validateMethodOverrideSignature compared returnsByRef with exact equality,
rejecting this valid program. Make the check one-directional; dropping a
parent's by-ref return remains fatal.
master
Alessio Giacobbe 11 hours ago committed by GitHub
parent 407953c094
commit 0c69b8b357
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      phpunit/bootstrap.php
  2. 19
      phpunit/code/override_byref_return_added.php
  3. 19
      phpunit/code/override_byref_return_dropped.php
  4. 24
      phpunit/src/MethodOverrideByRefReturnTest.php
  5. 5
      src/Translator.php

@ -5,6 +5,21 @@ use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
require __DIR__ . '/../bin/bootstrap.php';
// The vendor directory may be shared between checkouts (e.g. a git worktree
// with a symlinked vendor/). Composer's autoloader resolves TypePhp\ against
// the checkout that owns vendor/, which would silently test another tree's
// sources. Prepend a loader anchored to THIS checkout so the test suite always
// exercises the code it ships with.
spl_autoload_register(static function (string $class): void {
if (str_starts_with($class, 'TypePhp\\')) {
$path = dirname(__DIR__) . '/src/' . str_replace('\\', '/', substr($class, strlen('TypePhp\\'))) . '.php';
if (is_file($path)) {
require $path;
}
}
}, true, true);
require_once __DIR__ . '/../src/polyfills.php';
require __DIR__ . '/../src/gen_stub.php';

@ -0,0 +1,19 @@
<?php
class A
{
public function f(): array
{
return [];
}
}
class B extends A
{
public function &f(): array
{
static $a = [];
return $a;
}
}
function main() {}

@ -0,0 +1,19 @@
<?php
class A
{
public function &f(): array
{
static $a = [];
return $a;
}
}
class B extends A
{
public function f(): array
{
return [];
}
}
function main() {}

@ -0,0 +1,24 @@
<?php
use TypePhp\Exception\TestError;
/**
* Zend treats by-ref returns as covariant in overrides: a child method may add
* `&` to its return (callers expecting a value still work), but it must not
* drop a by-ref return promised by the parent contract.
*/
class MethodOverrideByRefReturnTest extends BaseTest
{
public function testOverrideMayAddByRefReturn(): void
{
$this->compile('override_byref_return_added.php');
}
public function testOverrideCannotDropByRefReturn(): void
{
$this->exec(
'Declaration of `B::f()` must be compatible with `A::f()`',
'override_byref_return_dropped.php',
);
}
}

@ -4674,7 +4674,10 @@ CODE;
)) {
$this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass);
}
if ($childFuncDef->returnsByRef !== $parentFuncDef->returnsByRef) {
// Zend treats by-ref returns as covariant: an override may add `&`
// (callers expecting a value still work), but it must not drop one
// promised by the parent contract.
if ($parentFuncDef->returnsByRef && !$childFuncDef->returnsByRef) {
$this->fatalMethodOverrideIncompatible($v, $className, $methodName, $parentClass);
}

Loading…
Cancel
Save