Skip to content

Commit

Permalink
Support full variance if autoloading is used
Browse files Browse the repository at this point in the history
Keep track of delayed variance obligations and check them after
linking a class is otherwise finished. Obligations may either be
unresolved method compatibility (because the necessecary classes
aren't available yet) or open parent/interface dependencies. The
latter occur because we allow the use of not fully linked classes
as parents/interfaces now.

An important aspect of the implementation is we do not require
classes involved in variance checks to be fully linked in order for
the class to be fully linked. Because the involved types do have to
exist in the class table (as partially linked classes) and we do
check these for correct variance, we have the guarantee that either
those classes will successfully link lateron or generate an error,
but there is no way to actually use them until that point and as
such no possibility of violating the variance contract. This is
important because it ensures that a class declaration always either
errors or will produce an immediately usable class afterwards --
there are no cases where the finalization of the class declaration
has to be delayed until a later time, as earlier variants of this
patch did.

Because variance checks deal with classes in various stages of
linking, we need to use a special instanceof implementation that
supports this, and also introduce finer-grained flags that tell us
which parts have been linked already and which haven't.

Class autoloading for variance checks is delayed into a separate
stage after the class is otherwise linked and before delayed
variance obligations are processed. This separation is needed to
handle cases like A extends B extends C, where B is the autoload
root, but C is required to check variance. This could end up
loading C while the class structure of B is in an inconsistent
state.
  • Loading branch information
nikic committed Jun 11, 2019
1 parent 89b2d88 commit 8f8fcbb
Show file tree
Hide file tree
Showing 23 changed files with 762 additions and 69 deletions.
4 changes: 3 additions & 1 deletion UPGRADING
Expand Up @@ -163,7 +163,9 @@ PHP 7.4 UPGRADE NOTES
public function method(): B {}
}

This feature is currently restricted to non-cyclic type references only.
Full variance support is only available if autoloading is used. Inside a
single file only non-cyclic type references are possible, because all
classes need to be available before they are referenced.
RFC: https://wiki.php.net/rfc/covariant-returns-and-contravariant-parameters

. Added support for coalesce assign (??=) operator. For example:
Expand Down
2 changes: 1 addition & 1 deletion Zend/tests/bug30922.phpt
Expand Up @@ -10,4 +10,4 @@ var_dump($a instanceOf A);
echo "ok\n";
?>
--EXPECTF--
Fatal error: Interface 'RecurisiveFooFar' not found in %sbug30922.php on line %d
Fatal error: Interface RecurisiveFooFar cannot implement itself in %s on line %d
37 changes: 37 additions & 0 deletions Zend/tests/type_declarations/variance/class_order_autoload1.phpt
@@ -0,0 +1,37 @@
--TEST--
Class order allowed with autoloading (1)
--FILE--
<?php

spl_autoload_register(function($class) {
if ($class === 'A') {
class A {
public function method() : B {}
}
var_dump(new A);
} else if ($class == 'B') {
class B extends A {
public function method() : C {}
}
var_dump(new B);
} else {
class C extends B {
}
var_dump(new C);
}
});

var_dump(new C);

?>
===DONE===
--EXPECT--
object(A)#2 (0) {
}
object(B)#2 (0) {
}
object(C)#2 (0) {
}
object(C)#2 (0) {
}
===DONE===
38 changes: 38 additions & 0 deletions Zend/tests/type_declarations/variance/class_order_autoload2.phpt
@@ -0,0 +1,38 @@
--TEST--
Class order allowed with autoloading (2)
--FILE--
<?php

spl_autoload_register(function($class) {
if ($class === 'A') {
class A {
public function method() : B {}
}
var_dump(new A);
} else if ($class == 'B') {
class B extends A {
public function method() : C {}
}
var_dump(new B);
} else {
class C extends B {
}
var_dump(new C);
}
});

// Same as autoload1 test case, but with a different autoloading root
var_dump(new B);

?>
===DONE===
--EXPECT--
object(A)#2 (0) {
}
object(C)#2 (0) {
}
object(B)#2 (0) {
}
object(B)#2 (0) {
}
===DONE===
45 changes: 45 additions & 0 deletions Zend/tests/type_declarations/variance/class_order_autoload3.phpt
@@ -0,0 +1,45 @@
--TEST--
Class order allowed with autoloading (3)
--FILE--
<?php

spl_autoload_register(function($class) {
if ($class == 'A') {
class A {
public function method(): X {}
}
var_dump(new A);
} else if ($class == 'B') {
class B extends A {
public function method(): Y {}
}
var_dump(new B);
} else if ($class == 'X') {
class X {
public function method(): A {}
}
var_dump(new X);
} else if ($class == 'Y') {
class Y extends X {
public function method(): B {}
}
var_dump(new Y);
}
});

var_dump(new B);

?>
===DONE===
--EXPECT--
object(A)#2 (0) {
}
object(X)#2 (0) {
}
object(Y)#2 (0) {
}
object(B)#2 (0) {
}
object(B)#2 (0) {
}
===DONE===
44 changes: 44 additions & 0 deletions Zend/tests/type_declarations/variance/class_order_autoload4.phpt
@@ -0,0 +1,44 @@
--TEST--
Class order allowed with autoloading (4)
--FILE--
<?php

// Same as autoload3 test case, but with X, Y being interfaces.
spl_autoload_register(function($class) {
if ($class == 'A') {
class A {
public function method(): X {}
}
var_dump(new A);
} else if ($class == 'B') {
class B extends A {
public function method(): Y {}
}
var_dump(new B);
} else if ($class == 'X') {
interface X {
public function method(): A;
}
var_dump(interface_exists('X'));
} else if ($class == 'Y') {
interface Y extends X {
public function method(): B;
}
var_dump(interface_exists('Y'));
}
});

var_dump(new B);

?>
===DONE===
--EXPECT--
object(A)#2 (0) {
}
bool(true)
bool(true)
object(B)#2 (0) {
}
object(B)#2 (0) {
}
===DONE===
60 changes: 60 additions & 0 deletions Zend/tests/type_declarations/variance/class_order_autoload5.phpt
@@ -0,0 +1,60 @@
--TEST--
Class order allowed with autoloading (5)
--FILE--
<?php

// Similar to variance3, but one more class hierarchy in the cycle
spl_autoload_register(function($class) {
if ($class == 'A') {
class A {
public function method(): X {}
}
var_dump(new A);
} else if ($class == 'B') {
class B extends A {
public function method(): Y {}
}
var_dump(new B);
} else if ($class == 'X') {
class X {
public function method(): Q {}
}
var_dump(new X);
} else if ($class == 'Y') {
class Y extends X {
public function method(): R {}
}
var_dump(new Y);
} else if ($class == 'Q') {
class Q {
public function method(): A {}
}
var_dump(new Q);
} else if ($class == 'R') {
class R extends Q {
public function method(): B {}
}
var_dump(new R);
}
});

var_dump(new B);

?>
===DONE===
--EXPECT--
object(A)#2 (0) {
}
object(X)#2 (0) {
}
object(Q)#2 (0) {
}
object(R)#2 (0) {
}
object(Y)#2 (0) {
}
object(B)#2 (0) {
}
object(B)#2 (0) {
}
===DONE===
@@ -1,25 +1,25 @@
--TEST--
Returns are covariant, but we don't allow the code due to class ordering (autoload variation)
Variance error in the presence of autoloading (1)
--FILE--
<?php

spl_autoload_register(function($class) {
if ($class === 'A') {
class A {
public function method() : B {}
public function method() : C {}
}
} else if ($class == 'B') {
class B extends A {
public function method() : C {}
public function method() : B {}
}
} else {
class C extends B {
}
}
});

$c = new C;
$b = new B;

?>
--EXPECTF--
Fatal error: Could not check compatibility between B::method(): C and A::method(): B, because class C is not available in %s on line %d
Fatal error: Declaration of B::method(): B must be compatible with A::method(): C in %s on line %d
@@ -0,0 +1,27 @@
--TEST--
Variance error in the presence of autoloading (2)
--FILE--
<?php

// Same as autoload_error1, but for argument types.
spl_autoload_register(function($class) {
if ($class === 'A') {
class A {
public function method(B $x) {}
}
} else if ($class == 'B') {
class B extends A {
public function method(C $x) {}
}
} else {
class C extends B {
}
}
});

$b = new B;
$c = new C;

?>
--EXPECTF--
Warning: Declaration of B::method(C $x) should be compatible with A::method(B $x) in %s on line %d
@@ -0,0 +1,38 @@
--TEST--
Variance error in the presence of autoloading (3)
--FILE--
<?php

spl_autoload_register(function($class) {
if ($class == 'A') {
class A {
public function method(): X {}
}
} else if ($class == 'B') {
class B extends A {
public function method(): Y {}
}
} else if ($class == 'X') {
class X {
public function method(): Q {}
}
} else if ($class == 'Y') {
class Y extends X {
public function method(): R {}
}
} else if ($class == 'Q') {
class Q {
public function method(): B {}
}
} else if ($class == 'R') {
class R extends Q {
public function method(): A {}
}
}
});

$b = new B;

?>
--EXPECTF--
Fatal error: Declaration of R::method(): A must be compatible with Q::method(): B in %s on line %d
@@ -0,0 +1,39 @@
--TEST--
Variance error in the presence of autoloading (4)
--FILE--
<?php

spl_autoload_register(function($class) {
if ($class == 'A') {
class A {
public function method(): X {}
}
var_dump(new A);
} else if ($class == 'B') {
class B extends A {
public function method(): Y {}
}
var_dump(new B);
} else if ($class == 'X') {
class X {
public function method(): B {}
}
var_dump(new X);
} else if ($class == 'Y') {
class Y extends X {
public function method(): A {}
}
var_dump(new Y);
}
});

var_dump(new B);

?>
--EXPECTF--
object(A)#2 (0) {
}
object(X)#2 (0) {
}

Fatal error: Declaration of Y::method(): A must be compatible with X::method(): B in %s on line %d

0 comments on commit 8f8fcbb

Please sign in to comment.