Skip to content

Commit e72bf63

Browse files
committed
Allow variadic arguments to replace non-variadic ones
Any number of arguments can be replaced by a variadic one, so long as the variadic argument is compatible (in the sense of contravariance) with the subsumed arguments. In particular this means that function(...$args) becomes a near-universal signature: It is compatible with any function signature that does not accept parameters by-reference. This also fixes bug #70839, which describes a special case. Closes GH-5059.
1 parent 4da7e67 commit e72bf63

File tree

7 files changed

+102
-34
lines changed

7 files changed

+102
-34
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ PHP NEWS
1010
(Nikita)
1111
. Fixed bug #49555 (Fatal error "Function must be a string" message should be
1212
renamed). (Nikita)
13+
. Fixed bug #70839 (Convertion optional argument to variadic forbidden by LSP
14+
checks). (Nikita)
1315

1416
- CURL:
1517
. Bumped required libcurl version to 7.29.0. (cmb)

UPGRADING

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,17 @@ PHP 8.0 UPGRADE NOTES
351351
. Added WeakMap.
352352
RFC: https://wiki.php.net/rfc/weak_maps
353353
. Added ValueError class.
354+
. Any number of function parameters may not be replaced by a variadic
355+
argument, as long as the types are compatible. For example, the following
356+
code is now allowed:
357+
358+
class A {
359+
public function method(int $many, string $parameters, $here) {}
360+
}
361+
class B extends A {
362+
public function method(...$everything) {}
363+
}
364+
354365

355366
- Date:
356367
. Added DateTime::createFromInterface() and
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--TEST--
2+
Illegal variadic inheritance due to reference mismatch
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
public function test(&$a, &$b) {}
8+
}
9+
10+
class B extends A {
11+
public function test(...$args) {}
12+
}
13+
14+
?>
15+
--EXPECTF--
16+
Fatal error: Declaration of B::test(...$args) must be compatible with A::test(&$a, &$b) in %s on line %d
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--TEST--
2+
Illegal variadic inheritance due to type mismatch
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
public function test(int $a, int $b) {}
8+
}
9+
10+
class B extends A {
11+
public function test(string ...$args) {}
12+
}
13+
14+
?>
15+
--EXPECTF--
16+
Fatal error: Declaration of B::test(string ...$args) must be compatible with A::test(int $a, int $b) in %s on line %d
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
Cases where non-variadic parameters are allowed to be subsumed by a variadic one
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
public function test1($a, $b) {}
8+
public function test2(int $a, int $b) {}
9+
public function test3(int $a, int $b) {}
10+
public function test4(int $a, string $b) {}
11+
public function test5(&$a, &$b) {}
12+
}
13+
14+
class B extends A {
15+
public function test1(...$args) {}
16+
public function test2(...$args) {}
17+
public function test3(int ...$args) {}
18+
public function test4(int|string ...$args) {}
19+
public function test5(&...$args) {}
20+
}
21+
22+
?>
23+
===DONE==
24+
--EXPECT--
25+
===DONE==
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
--TEST--
2-
It's not possible to remove required parameter before a variadic parameter
2+
It is possible to remove required parameter before a variadic parameter
33
--FILE--
44
<?php
55

6-
/* Theoretically this should be valid because it weakens the constraint, but
7-
* PHP does not allow this (for non-variadics), so I'm not allowing it here, too,
8-
* to stay consistent. */
9-
106
interface DB {
117
public function query($query, ...$params);
128
}
@@ -16,5 +12,6 @@ class MySQL implements DB {
1612
}
1713

1814
?>
19-
--EXPECTF--
20-
Fatal error: Declaration of MySQL::query(...$params) must be compatible with DB::query($query, ...$params) in %s on line %d
15+
===DONE===
16+
--EXPECT--
17+
===DONE===

Zend/zend_inheritance.c

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,9 @@ static inheritance_status zend_do_perform_arg_type_hint_check(
495495
static inheritance_status zend_do_perform_implementation_check(
496496
const zend_function *fe, const zend_function *proto) /* {{{ */
497497
{
498-
uint32_t i, num_args;
498+
uint32_t i, num_args, proto_num_args, fe_num_args;
499499
inheritance_status status, local_status;
500+
zend_bool proto_is_variadic, fe_is_variadic;
500501

501502
/* If it's a user function then arg_info == NULL means we don't have any parameters but
502503
* we still need to do the arg number checks. We are only willing to ignore this for internal
@@ -516,9 +517,8 @@ static inheritance_status zend_do_perform_implementation_check(
516517
/* If the prototype method is private do not enforce a signature */
517518
ZEND_ASSERT(!(proto->common.fn_flags & ZEND_ACC_PRIVATE));
518519

519-
/* check number of arguments */
520-
if (proto->common.required_num_args < fe->common.required_num_args
521-
|| proto->common.num_args > fe->common.num_args) {
520+
/* The number of required arguments cannot increase. */
521+
if (proto->common.required_num_args < fe->common.required_num_args) {
522522
return INHERITANCE_ERROR;
523523
}
524524

@@ -528,35 +528,36 @@ static inheritance_status zend_do_perform_implementation_check(
528528
return INHERITANCE_ERROR;
529529
}
530530

531-
if ((proto->common.fn_flags & ZEND_ACC_VARIADIC)
532-
&& !(fe->common.fn_flags & ZEND_ACC_VARIADIC)) {
531+
proto_is_variadic = (proto->common.fn_flags & ZEND_ACC_VARIADIC) != 0;
532+
fe_is_variadic = (fe->common.fn_flags & ZEND_ACC_VARIADIC) != 0;
533+
534+
/* A variadic function cannot become non-variadic */
535+
if (proto_is_variadic && !fe_is_variadic) {
533536
return INHERITANCE_ERROR;
534537
}
535538

536-
/* For variadic functions any additional (optional) arguments that were added must be
537-
* checked against the signature of the variadic argument, so in this case we have to
538-
* go through all the parameters of the function and not just those present in the
539-
* prototype. */
540-
num_args = proto->common.num_args;
541-
if (proto->common.fn_flags & ZEND_ACC_VARIADIC) {
542-
num_args++;
543-
if (fe->common.num_args >= proto->common.num_args) {
544-
num_args = fe->common.num_args;
545-
if (fe->common.fn_flags & ZEND_ACC_VARIADIC) {
546-
num_args++;
547-
}
548-
}
549-
}
539+
/* The variadic argument is not included in the stored argument count. */
540+
proto_num_args = proto->common.num_args + proto_is_variadic;
541+
fe_num_args = fe->common.num_args + fe_is_variadic;
542+
num_args = MAX(proto_num_args, fe_num_args);
550543

551544
status = INHERITANCE_SUCCESS;
552545
for (i = 0; i < num_args; i++) {
553-
zend_arg_info *fe_arg_info = &fe->common.arg_info[i];
554-
555-
zend_arg_info *proto_arg_info;
556-
if (i < proto->common.num_args) {
557-
proto_arg_info = &proto->common.arg_info[i];
558-
} else {
559-
proto_arg_info = &proto->common.arg_info[proto->common.num_args];
546+
zend_arg_info *proto_arg_info =
547+
i < proto_num_args ? &proto->common.arg_info[i] :
548+
proto_is_variadic ? &proto->common.arg_info[proto_num_args - 1] : NULL;
549+
zend_arg_info *fe_arg_info =
550+
i < fe_num_args ? &fe->common.arg_info[i] :
551+
fe_is_variadic ? &fe->common.arg_info[fe_num_args - 1] : NULL;
552+
if (!proto_arg_info) {
553+
/* A new (optional) argument has been added, which is fine. */
554+
continue;
555+
}
556+
if (!fe_arg_info) {
557+
/* An argument has been removed. This is considered illegal, because arity checks
558+
* work based on a model where passing more than the declared number of parameters
559+
* to a function is an error. */
560+
return INHERITANCE_ERROR;
560561
}
561562

562563
local_status = zend_do_perform_arg_type_hint_check(fe, fe_arg_info, proto, proto_arg_info);

0 commit comments

Comments
 (0)