Skip to content

Fix prototype for trait methods #14016

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions Zend/tests/gh14009_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
--TEST--
GH-14009: Traits inherit prototype
--FILE--
<?php

class P {
protected function common() {
throw new Exception('Unreachable');
}
}

class A extends P {
public function test(P $sibling) {
$sibling->common();
}
}

class B extends P {
protected function common() {
echo __METHOD__, "\n";
}
}

trait T {
protected function common() {
echo __METHOD__, "\n";
}
}

class C extends P {
use T;
}

$a = new A();
$a->test(new B());
$a->test(new C());

?>
--EXPECT--
B::common
T::common
22 changes: 22 additions & 0 deletions Zend/tests/gh14009_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
GH-14009: Traits inherit prototype
--FILE--
<?php

class P {
protected function common() {}
}

class A extends P {}

trait T {
private abstract function common(int $param);
}

class B extends P {
use T;
}

?>
--EXPECTF--
Fatal error: Declaration of P::common() must be compatible with T::common(int $param) in %s on line %d
19 changes: 19 additions & 0 deletions Zend/tests/gh14009_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--TEST--
GH-14009: Traits inherit prototype
--FILE--
<?php

class A {
private function test() {}
}

trait T {
private function test() {}
}

class B extends A {
use T;
}

?>
--EXPECT--
37 changes: 37 additions & 0 deletions Zend/tests/gh14009_004.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
GH-14009: Traits inherit prototype
--FILE--
<?php

class A {
protected function test() {
throw new Exception('Unreachable');
}
}

class B extends A {
// Prototype is A::test().
protected function test() {
echo __METHOD__, "\n";
}
}

trait T {
protected abstract function test();
}

class C extends B {
use T;
}

class D extends A {
public static function callTest($sibling) {
$sibling->test();
}
}

D::callTest(new C());

?>
--EXPECT--
B::test
19 changes: 19 additions & 0 deletions Zend/tests/interface_constructor_prototype_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--TEST--
Interfaces don't set prototypes to their parent method
--FILE--
<?php

interface A {
public function __construct(int $param);
}
interface B extends A {
public function __construct(int|float $param);
}
class Test implements B {
public function __construct(int $param) {}
}
new Test(42);

?>
--EXPECTF--
Fatal error: Declaration of Test::__construct(int $param) must be compatible with B::__construct(int|float $param) in %s on line %d
26 changes: 26 additions & 0 deletions Zend/tests/interface_constructor_prototype_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Interfaces don't set prototypes to their parent method
--XFAIL--
X::__constructor()'s prototype is set to B::__construct(). Y::__construct() then
uses prototype to verify LSP, but misses A::__construct() which has a stricter
signature.
--FILE--
<?php

interface A {
public function __construct(int|float $param);
}
interface B {
public function __construct(int $param);
}
class X implements A, B {
public function __construct(int|float $param) {}
}
class Y extends X {
public function __construct(int $param) {}
}
new Y(42);

?>
--EXPECTF--
Fatal error: Declaration of Y::__construct(int $param) must be compatible with A::__construct(int|float $param) in %s on line %d
46 changes: 30 additions & 16 deletions Zend/zend_inheritance.c
Original file line number Diff line number Diff line change
Expand Up @@ -1085,8 +1085,19 @@ static zend_always_inline inheritance_status do_inheritance_check_on_method_ex(
uint32_t parent_flags = parent->common.fn_flags;
zend_function *proto;

#define SEPARATE_METHOD() do { \
if (child_scope != ce && child->type == ZEND_USER_FUNCTION && child_zv) { \
/* op_array wasn't duplicated yet */ \
zend_function *new_function = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); \
memcpy(new_function, child, sizeof(zend_op_array)); \
Z_PTR_P(child_zv) = child = new_function; \
child_zv = NULL; \
} \
} while(0)

if (UNEXPECTED((parent_flags & ZEND_ACC_PRIVATE) && !(parent_flags & ZEND_ACC_ABSTRACT) && !(parent_flags & ZEND_ACC_CTOR))) {
if (!check_only) {
SEPARATE_METHOD();
child->common.fn_flags |= ZEND_ACC_CHANGED;
}
/* The parent method is private and not an abstract so we don't need to check any inheritance rules */
Expand Down Expand Up @@ -1130,7 +1141,12 @@ static zend_always_inline inheritance_status do_inheritance_check_on_method_ex(
ZEND_FN_SCOPE_NAME(parent), ZSTR_VAL(child->common.function_name), ZEND_FN_SCOPE_NAME(child));
}

if (!check_only && (parent_flags & (ZEND_ACC_PRIVATE|ZEND_ACC_CHANGED))) {
if (!check_only
&& (parent_flags & (ZEND_ACC_PRIVATE|ZEND_ACC_CHANGED))
/* In the case of abstract traits, don't mark the method as changed. We don't actually have a
* private parent method. */
&& !(parent->common.scope->ce_flags & ZEND_ACC_TRAIT)) {
SEPARATE_METHOD();
child->common.fn_flags |= ZEND_ACC_CHANGED;
}

Expand All @@ -1146,21 +1162,16 @@ static zend_always_inline inheritance_status do_inheritance_check_on_method_ex(
parent = proto;
}

if (!check_only && child->common.prototype != proto && child_zv) {
do {
if (child->common.scope != ce && child->type == ZEND_USER_FUNCTION) {
if (ce->ce_flags & ZEND_ACC_INTERFACE) {
/* Few parent interfaces contain the same method */
break;
} else {
/* op_array wasn't duplicated yet */
zend_function *new_function = zend_arena_alloc(&CG(arena), sizeof(zend_op_array));
memcpy(new_function, child, sizeof(zend_op_array));
Z_PTR_P(child_zv) = child = new_function;
}
}
child->common.prototype = proto;
} while (0);
if (!check_only
&& child->common.prototype != proto
/* We are not setting the prototype of overridden interface methods because of abstract
* constructors. See Zend/tests/interface_constructor_prototype_001.phpt. */
&& !(ce->ce_flags & ZEND_ACC_INTERFACE)
/* Parent is a trait when its method is abstract. We only need to verify its signature then,
* don't actually use it as a prototype. See Zend/tests/gh14009_004.phpt. */
&& !(parent->common.scope->ce_flags & ZEND_ACC_TRAIT)) {
Comment on lines +1170 to +1172
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this comment is true, then traits should use this function with check_only == true, but check_only also hides some error messages and makes test failures :(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that was my first thought. But I then realized that this completely disables the error messages, and adding yet another parameter to the already confusing ones is something I was trying to avoid.

SEPARATE_METHOD();
child->common.prototype = proto;
}

/* Prevent derived classes from restricting access that was available in parent classes (except deriving from non-abstract ctors) */
Expand All @@ -1180,6 +1191,9 @@ static zend_always_inline inheritance_status do_inheritance_check_on_method_ex(
}
perform_delayable_implementation_check(ce, child, child_scope, parent, parent_scope);
}

#undef SEPARATE_METHOD

return INHERITANCE_SUCCESS;
}
/* }}} */
Expand Down