Commit c9249e2d3aa for php.net

commit c9249e2d3aa401bda5d9a3071e86e0594807ed00
Author: Tim Düsterhus <tim@tideways-gmbh.com>
Date:   Mon Jun 30 12:31:27 2025 +0200

    Support every argument syntax for `clone()` (#18938)

    * zend_language_parser: Support every argument syntax for `clone()`

    * zend_language_parser: Adjust `clone()` grammar to avoid conflicts

    * zend_language_parser: Add explanatory comment for `clone_argument_list`

diff --git a/Zend/tests/clone/ast.phpt b/Zend/tests/clone/ast.phpt
index 89a1a0a4810..e482854a944 100644
--- a/Zend/tests/clone/ast.phpt
+++ b/Zend/tests/clone/ast.phpt
@@ -18,6 +18,60 @@
 	echo $e->getMessage(), PHP_EOL;
 }

+try {
+	assert(false && $y = clone($x, ));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(false && $y = clone($x, [ "foo" => $foo, "bar" => $bar ]));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(false && $y = clone($x, $array));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(false && $y = clone($x, $array, $extraParameter, $trailingComma, ));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(false && $y = clone(object: $x, withProperties: [ "foo" => $foo, "bar" => $bar ]));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(false && $y = clone($x, withProperties: [ "foo" => $foo, "bar" => $bar ]));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(false && $y = clone(object: $x));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(false && $y = clone(object: $x, [ "foo" => $foo, "bar" => $bar ]));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(false && $y = clone(...["object" => $x, "withProperties" => [ "foo" => $foo, "bar" => $bar ]]));
+} catch (Error $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
 try {
 	assert(false && $y = clone(...));
 } catch (Error $e) {
@@ -28,4 +82,13 @@
 --EXPECT--
 assert(false && ($y = \clone($x)))
 assert(false && ($y = \clone($x)))
+assert(false && ($y = \clone($x)))
+assert(false && ($y = \clone($x, ['foo' => $foo, 'bar' => $bar])))
+assert(false && ($y = \clone($x, $array)))
+assert(false && ($y = \clone($x, $array, $extraParameter, $trailingComma)))
+assert(false && ($y = \clone(object: $x, withProperties: ['foo' => $foo, 'bar' => $bar])))
+assert(false && ($y = \clone($x, withProperties: ['foo' => $foo, 'bar' => $bar])))
+assert(false && ($y = \clone(object: $x)))
+assert(false && ($y = \clone(object: $x, ['foo' => $foo, 'bar' => $bar])))
+assert(false && ($y = \clone(...['object' => $x, 'withProperties' => ['foo' => $foo, 'bar' => $bar]])))
 assert(false && ($y = \clone(...)))
diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y
index 016c6e5c9d0..805f378cb98 100644
--- a/Zend/zend_language_parser.y
+++ b/Zend/zend_language_parser.y
@@ -259,7 +259,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
 %type <ast> unprefixed_use_declarations const_decl inner_statement
 %type <ast> expr optional_expr while_statement for_statement foreach_variable
 %type <ast> foreach_statement declare_statement finally_statement unset_variable variable
-%type <ast> extends_from parameter optional_type_without_static argument global_var
+%type <ast> extends_from parameter optional_type_without_static argument argument_no_expr global_var
 %type <ast> static_var class_statement trait_adaptation trait_precedence trait_alias
 %type <ast> absolute_trait_method_reference trait_method_reference property echo_expr
 %type <ast> new_dereferenceable new_non_dereferenceable anonymous_class class_name class_name_reference simple_variable
@@ -287,7 +287,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*);
 %type <ast> enum_declaration_statement enum_backing_type enum_case enum_case_expr
 %type <ast> function_name non_empty_member_modifiers
 %type <ast> property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body
-%type <ast> optional_parameter_list
+%type <ast> optional_parameter_list clone_argument_list non_empty_clone_argument_list

 %type <num> returns_ref function fn is_reference is_variadic property_modifiers property_hook_modifiers
 %type <num> method_modifiers class_const_modifiers member_modifier optional_cpp_modifiers
@@ -914,13 +914,42 @@ non_empty_argument_list:
 			{ $$ = zend_ast_list_add($1, $3); }
 ;

-argument:
-		expr				{ $$ = $1; }
-	|	identifier ':' expr
+/* `clone_argument_list` is necessary to resolve a parser ambiguity (shift-reduce conflict)
+ * of `clone($expr)`, which could either be parsed as a function call with `$expr` as the first
+ * argument or as a use of the `clone` language construct with an expression with useless
+ * parenthesis. Both would be valid and result in the same AST / the same semantics.
+ * `clone_argument_list` is defined in a way that an `expr` in the first position needs to
+ * be followed by a `,` which is not valid syntax for a parenthesized `expr`, ensuring
+ * that calling `clone()` with a single unnamed parameter is handled by the language construct
+ * syntax.
+ */
+clone_argument_list:
+		'(' ')'	{ $$ = zend_ast_create_list(0, ZEND_AST_ARG_LIST); }
+	|	'(' non_empty_clone_argument_list possible_comma ')' { $$ = $2; }
+	|	'(' expr ',' ')' { $$ = zend_ast_create_list(1, ZEND_AST_ARG_LIST, $2); }
+	|	'(' T_ELLIPSIS ')' { $$ = zend_ast_create_fcc(); }
+;
+
+non_empty_clone_argument_list:
+		expr ',' argument
+			{ $$ = zend_ast_create_list(2, ZEND_AST_ARG_LIST, $1, $3); }
+	|	argument_no_expr
+			{ $$ = zend_ast_create_list(1, ZEND_AST_ARG_LIST, $1); }
+	|	non_empty_clone_argument_list ',' argument
+			{ $$ = zend_ast_list_add($1, $3); }
+;
+
+argument_no_expr:
+		identifier ':' expr
 			{ $$ = zend_ast_create(ZEND_AST_NAMED_ARG, $1, $3); }
 	|	T_ELLIPSIS expr	{ $$ = zend_ast_create(ZEND_AST_UNPACK, $2); }
 ;

+argument:
+		expr { $$ = $1; }
+	|	argument_no_expr { $$ = $1; }
+;
+
 global_var_list:
 		global_var_list ',' global_var { $$ = zend_ast_list_add($1, $3); }
 	|	global_var { $$ = zend_ast_create_list(1, ZEND_AST_STMT_LIST, $1); }
@@ -1228,10 +1257,10 @@ expr:
 			{ $$ = zend_ast_create(ZEND_AST_ASSIGN, $1, $3); }
 	|	variable '=' ampersand variable
 			{ $$ = zend_ast_create(ZEND_AST_ASSIGN_REF, $1, $4); }
-	|	T_CLONE '(' T_ELLIPSIS ')' {
+	|	T_CLONE clone_argument_list {
 			zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE));
 			name->attr = ZEND_NAME_FQ;
-			$$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_fcc());
+			$$ = zend_ast_create(ZEND_AST_CALL, name, $2);
 		}
 	|	T_CLONE expr {
 			zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE));