Skip to content

Commit afc4d67

Browse files
committed
Consistently treat optional-before-required as required
There was a loophole here when it came to usage with named arguments, which was not intended. Close the loophole thoroughly by actually dropping the default value from the signature entirely. The default is still used to make the type nullable, but not for anything else.
1 parent 7331bc7 commit afc4d67

File tree

5 files changed

+49
-15
lines changed

5 files changed

+49
-15
lines changed

Zend/tests/call_user_func_005.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var_dump(call_user_func(array('foo', 'teste')));
1818

1919
?>
2020
--EXPECTF--
21-
Deprecated: Required parameter $b follows optional parameter $a in %s on line %d
21+
Deprecated: Optional parameter $a declared before required parameter $b is implicitly treated as a required parameter in %s on line %d
2222
string(1) "x"
2323
array(1) {
2424
[0]=>

Zend/tests/required_param_after_optional.phpt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ function test3(Type $test3A = null, Type2 $test3B = null, $test3C) {}
99

1010
?>
1111
--EXPECTF--
12-
Deprecated: Required parameter $testC follows optional parameter $testA in %s on line %d
12+
Deprecated: Optional parameter $testA declared before required parameter $testC is implicitly treated as a required parameter in %s on line %d
1313

14-
Deprecated: Required parameter $test2C follows optional parameter $test2B in %s on line %d
14+
Deprecated: Optional parameter $testB declared before required parameter $testC is implicitly treated as a required parameter in %s on line %d
15+
16+
Deprecated: Optional parameter $test2B declared before required parameter $test2C is implicitly treated as a required parameter in %s on line %d
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Optional param before required should be treated as required for named args as well
3+
--FILE--
4+
<?php
5+
6+
function test($a = 1, $b) {
7+
}
8+
try {
9+
test(b: 2);
10+
} catch (Error $e) {
11+
echo $e->getMessage(), "\n";
12+
}
13+
14+
?>
15+
--EXPECTF--
16+
Deprecated: Optional parameter $a declared before required parameter $b is implicitly treated as a required parameter in %s on line %d
17+
test(): Argument #1 ($a) not passed

Zend/zend_compile.c

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6466,7 +6466,6 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
64666466
uint32_t i;
64676467
zend_op_array *op_array = CG(active_op_array);
64686468
zend_arg_info *arg_infos;
6469-
zend_string *optional_param = NULL;
64706469

64716470
if (return_type_ast || fallback_return_type) {
64726471
/* Use op_array->arg_info[-1] for return type */
@@ -6489,6 +6488,17 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
64896488
arg_infos = safe_emalloc(sizeof(zend_arg_info), list->children, 0);
64906489
}
64916490

6491+
/* Find last required parameter number for deprecation message. */
6492+
uint32_t last_required_param = (uint32_t) -1;
6493+
for (i = 0; i < list->children; ++i) {
6494+
zend_ast *param_ast = list->child[i];
6495+
zend_ast *default_ast_ptr = param_ast->child[2];
6496+
bool is_variadic = (param_ast->attr & ZEND_PARAM_VARIADIC) != 0;
6497+
if (!default_ast_ptr && !is_variadic) {
6498+
last_required_param = i;
6499+
}
6500+
}
6501+
64926502
for (i = 0; i < list->children; ++i) {
64936503
zend_ast *param_ast = list->child[i];
64946504
zend_ast *type_ast = param_ast->child[0];
@@ -6544,23 +6554,30 @@ void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32_t fall
65446554
zend_const_expr_to_zval(&default_node.u.constant, default_ast_ptr);
65456555
CG(compiler_options) = cops;
65466556

6547-
if (!optional_param) {
6557+
if (last_required_param != (uint32_t) -1 && i < last_required_param) {
65486558
/* Ignore parameters of the form "Type $param = null".
65496559
* This is the PHP 5 style way of writing "?Type $param", so allow it for now. */
65506560
bool is_implicit_nullable =
65516561
type_ast && Z_TYPE(default_node.u.constant) == IS_NULL;
65526562
if (!is_implicit_nullable) {
6553-
optional_param = name;
6563+
zend_ast *required_param_ast = list->child[last_required_param];
6564+
zend_error(E_DEPRECATED,
6565+
"Optional parameter $%s declared before required parameter $%s "
6566+
"is implicitly treated as a required parameter",
6567+
ZSTR_VAL(name), ZSTR_VAL(zend_ast_get_str(required_param_ast->child[1])));
65546568
}
6569+
6570+
/* Regardless of whether we issue a deprecation, convert this parameter into
6571+
* a required parameter without a default value. This ensures that it cannot be
6572+
* used as an optional parameter even with named parameters. */
6573+
opcode = ZEND_RECV;
6574+
default_node.op_type = IS_UNUSED;
6575+
zval_ptr_dtor(&default_node.u.constant);
65556576
}
65566577
} else {
65576578
opcode = ZEND_RECV;
65586579
default_node.op_type = IS_UNUSED;
65596580
op_array->required_num_args = i + 1;
6560-
if (optional_param) {
6561-
zend_error(E_DEPRECATED, "Required parameter $%s follows optional parameter $%s",
6562-
ZSTR_VAL(name), ZSTR_VAL(optional_param));
6563-
}
65646581
}
65656582

65666583
arg_info = &arg_infos[i];

ext/reflection/tests/bug62715.phpt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ foreach ($r->getParameters() as $p) {
1717
}
1818
?>
1919
--EXPECTF--
20-
Deprecated: Required parameter $c follows optional parameter $b in %s on line %d
21-
bool(true)
22-
bool(true)
20+
Deprecated: Optional parameter $b declared before required parameter $c is implicitly treated as a required parameter in %s on line %d
21+
bool(false)
22+
bool(false)
2323
bool(false)
24-
NULL
25-
int(0)

0 commit comments

Comments
 (0)