diff --git a/Zend/tests/gh11488.phpt b/Zend/tests/gh11488.phpt new file mode 100644 index 0000000000000..fe0128608af63 --- /dev/null +++ b/Zend/tests/gh11488.phpt @@ -0,0 +1,16 @@ +--TEST-- +GH-11488: "Optional parameter before required" warning for union nullable type +--FILE-- + +--EXPECTF-- +Deprecated: Optional parameter $a declared before required parameter $b is implicitly treated as a required parameter in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 0b54823d2026c..0507102836ddc 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6500,8 +6500,10 @@ static void zend_is_type_list_redundant_by_single_type(zend_type_list *type_list } } -static zend_type zend_compile_typename( - zend_ast *ast, bool force_allow_null) /* {{{ */ +static zend_type zend_compile_typename(zend_ast *ast, bool force_allow_null); + +static zend_type zend_compile_typename_ex( + zend_ast *ast, bool force_allow_null, bool *forced_allow_null) /* {{{ */ { bool is_marked_nullable = ast->attr & ZEND_TYPE_NULLABLE; zend_ast_attr orig_ast_attr = ast->attr; @@ -6704,6 +6706,10 @@ static zend_type zend_compile_typename( zend_error_noreturn(E_COMPILE_ERROR, "null cannot be marked as nullable"); } + if (force_allow_null && !is_marked_nullable && !(type_mask & MAY_BE_NULL)) { + *forced_allow_null = true; + } + if (is_marked_nullable || force_allow_null) { ZEND_TYPE_FULL_MASK(type) |= MAY_BE_NULL; type_mask = ZEND_TYPE_PURE_MASK(type); @@ -6722,6 +6728,12 @@ static zend_type zend_compile_typename( } /* }}} */ +static zend_type zend_compile_typename(zend_ast *ast, bool force_allow_null) +{ + bool forced_allow_null; + return zend_compile_typename_ex(ast, force_allow_null, &forced_allow_null); +} + /* May convert value from int to float. */ static bool zend_is_valid_default_value(zend_type type, zval *value) { @@ -6951,28 +6963,6 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 zend_const_expr_to_zval( &default_node.u.constant, default_ast_ptr, /* allow_dynamic */ true); CG(compiler_options) = cops; - - if (last_required_param != (uint32_t) -1 && i < last_required_param) { - /* Ignore parameters of the form "Type $param = null". - * This is the PHP 5 style way of writing "?Type $param", so allow it for now. */ - bool is_implicit_nullable = - type_ast && !(type_ast->attr & ZEND_TYPE_NULLABLE) - && Z_TYPE(default_node.u.constant) == IS_NULL; - if (!is_implicit_nullable) { - zend_ast *required_param_ast = list->child[last_required_param]; - zend_error(E_DEPRECATED, - "Optional parameter $%s declared before required parameter $%s " - "is implicitly treated as a required parameter", - ZSTR_VAL(name), ZSTR_VAL(zend_ast_get_str(required_param_ast->child[1]))); - } - - /* Regardless of whether we issue a deprecation, convert this parameter into - * a required parameter without a default value. This ensures that it cannot be - * used as an optional parameter even with named parameters. */ - opcode = ZEND_RECV; - default_node.op_type = IS_UNUSED; - zval_ptr_dtor(&default_node.u.constant); - } } else { opcode = ZEND_RECV; default_node.op_type = IS_UNUSED; @@ -6990,12 +6980,13 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 ); } + bool forced_allow_nullable = false; if (type_ast) { uint32_t default_type = *default_ast_ptr ? Z_TYPE(default_node.u.constant) : IS_UNDEF; bool force_nullable = default_type == IS_NULL && !property_flags; op_array->fn_flags |= ZEND_ACC_HAS_TYPE_HINTS; - arg_info->type = zend_compile_typename(type_ast, force_nullable); + arg_info->type = zend_compile_typename_ex(type_ast, force_nullable, &forced_allow_nullable); if (ZEND_TYPE_FULL_MASK(arg_info->type) & MAY_BE_VOID) { zend_error_noreturn(E_COMPILE_ERROR, "void cannot be used as a parameter type"); @@ -7014,6 +7005,26 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32 ZSTR_VAL(name), ZSTR_VAL(type_str)); } } + if (last_required_param != (uint32_t) -1 + && i < last_required_param + && default_node.op_type == IS_CONST) { + /* Ignore parameters of the form "Type $param = null". + * This is the PHP 5 style way of writing "?Type $param", so allow it for now. */ + if (!forced_allow_nullable) { + zend_ast *required_param_ast = list->child[last_required_param]; + zend_error(E_DEPRECATED, + "Optional parameter $%s declared before required parameter $%s " + "is implicitly treated as a required parameter", + ZSTR_VAL(name), ZSTR_VAL(zend_ast_get_str(required_param_ast->child[1]))); + } + + /* Regardless of whether we issue a deprecation, convert this parameter into + * a required parameter without a default value. This ensures that it cannot be + * used as an optional parameter even with named parameters. */ + opcode = ZEND_RECV; + default_node.op_type = IS_UNUSED; + zval_ptr_dtor(&default_node.u.constant); + } opline = zend_emit_op(NULL, opcode, NULL, &default_node); SET_NODE(opline->result, &var_node);