From 4272247ba3d0ee5a2e96d1b685fac3ce2ef78cde Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 14 Aug 2025 23:17:01 +0200 Subject: [PATCH] Prohibit pipe & arrow function combination that leads to confusing parse trees --- Zend/tests/arrow_functions/gh7900.phpt | 2 +- .../pipe_operator/mixed_callable_call.phpt | 2 +- Zend/tests/pipe_operator/prec_001.phpt | 12 ++++++++++ Zend/tests/pipe_operator/prec_002.phpt | 12 ++++++++++ Zend/tests/pipe_operator/prec_003.phpt | 12 ++++++++++ Zend/tests/pipe_operator/prec_004.phpt | 16 +++++++++++++ Zend/tests/pipe_operator/prec_005.phpt | 14 +++++++++++ Zend/tests/pipe_operator/prec_006.phpt | 13 ++++++++++ Zend/tests/pipe_operator/prec_007.phpt | 24 +++++++++++++++++++ Zend/zend_ast.c | 6 +++++ Zend/zend_ast.h | 2 +- Zend/zend_compile.c | 4 ++++ Zend/zend_compile.h | 3 +++ Zend/zend_language_parser.y | 1 + 14 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 Zend/tests/pipe_operator/prec_001.phpt create mode 100644 Zend/tests/pipe_operator/prec_002.phpt create mode 100644 Zend/tests/pipe_operator/prec_003.phpt create mode 100644 Zend/tests/pipe_operator/prec_004.phpt create mode 100644 Zend/tests/pipe_operator/prec_005.phpt create mode 100644 Zend/tests/pipe_operator/prec_006.phpt create mode 100644 Zend/tests/pipe_operator/prec_007.phpt diff --git a/Zend/tests/arrow_functions/gh7900.phpt b/Zend/tests/arrow_functions/gh7900.phpt index a4170fb1278fc..d6465c312399c 100644 --- a/Zend/tests/arrow_functions/gh7900.phpt +++ b/Zend/tests/arrow_functions/gh7900.phpt @@ -23,4 +23,4 @@ try { ?> --EXPECT-- Here -assert(fn(): never => 42 && false) +assert((fn(): never => 42) && false) diff --git a/Zend/tests/pipe_operator/mixed_callable_call.phpt b/Zend/tests/pipe_operator/mixed_callable_call.phpt index 55bae626f1890..d577f3aaefe69 100644 --- a/Zend/tests/pipe_operator/mixed_callable_call.phpt +++ b/Zend/tests/pipe_operator/mixed_callable_call.phpt @@ -71,7 +71,7 @@ $res1 = 1 |> [StaticTest::class, 'times17'] |> new Times23() |> $times29 - |> fn($x) => times2($x) + |> (fn($x) => times2($x)) ; var_dump($res1); diff --git a/Zend/tests/pipe_operator/prec_001.phpt b/Zend/tests/pipe_operator/prec_001.phpt new file mode 100644 index 0000000000000..3bd3397fd3617 --- /dev/null +++ b/Zend/tests/pipe_operator/prec_001.phpt @@ -0,0 +1,12 @@ +--TEST-- +Pipe precedence 001 +--FILE-- + fn($x) => $x < 42 + |> fn($x) => var_dump($x); + +?> +--EXPECTF-- +Fatal error: Arrow functions on the right hand side of |> must be parenthesized in %s on line %d diff --git a/Zend/tests/pipe_operator/prec_002.phpt b/Zend/tests/pipe_operator/prec_002.phpt new file mode 100644 index 0000000000000..73532b97e800c --- /dev/null +++ b/Zend/tests/pipe_operator/prec_002.phpt @@ -0,0 +1,12 @@ +--TEST-- +Pipe precedence 002 +--FILE-- + (fn($x) => $x < 42) + |> (fn($x) => var_dump($x)) ; + +?> +--EXPECT-- +bool(false) diff --git a/Zend/tests/pipe_operator/prec_003.phpt b/Zend/tests/pipe_operator/prec_003.phpt new file mode 100644 index 0000000000000..9200b8014e09f --- /dev/null +++ b/Zend/tests/pipe_operator/prec_003.phpt @@ -0,0 +1,12 @@ +--TEST-- +Pipe precedence 003 +--FILE-- + fn() => print (new Exception)->getTraceAsString() . "\n\n" + |> fn() => print (new Exception)->getTraceAsString() . "\n\n"; + +?> +--EXPECTF-- +Fatal error: Arrow functions on the right hand side of |> must be parenthesized in %s on line %d diff --git a/Zend/tests/pipe_operator/prec_004.phpt b/Zend/tests/pipe_operator/prec_004.phpt new file mode 100644 index 0000000000000..c04f483cdd8db --- /dev/null +++ b/Zend/tests/pipe_operator/prec_004.phpt @@ -0,0 +1,16 @@ +--TEST-- +Pipe precedence 004 +--FILE-- + (fn() => print (new Exception)->getTraceAsString() . "\n\n") + |> (fn() => print (new Exception)->getTraceAsString() . "\n\n"); + +?> +--EXPECTF-- +#0 %s(%d): {closure:%s:%d}(NULL) +#1 {main} + +#0 %s(%d): {closure:%s:%d}(1) +#1 {main} diff --git a/Zend/tests/pipe_operator/prec_005.phpt b/Zend/tests/pipe_operator/prec_005.phpt new file mode 100644 index 0000000000000..0dd262324e3d2 --- /dev/null +++ b/Zend/tests/pipe_operator/prec_005.phpt @@ -0,0 +1,14 @@ +--TEST-- +Pipe precedence 005 +--FILE-- + (fn() => 2)); +} catch (AssertionError $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +assert(false && 1 |> (fn() => 2)) diff --git a/Zend/tests/pipe_operator/prec_006.phpt b/Zend/tests/pipe_operator/prec_006.phpt new file mode 100644 index 0000000000000..d7fdf4c9b55b1 --- /dev/null +++ b/Zend/tests/pipe_operator/prec_006.phpt @@ -0,0 +1,13 @@ +--TEST-- +Pipe precedence 006 +--FILE-- + fn ($x) => $x ?? throw new Exception('Value may not be null') + |> fn ($x) => var_dump($x); + +?> +--EXPECTF-- +Fatal error: Arrow functions on the right hand side of |> must be parenthesized in %s on line %d diff --git a/Zend/tests/pipe_operator/prec_007.phpt b/Zend/tests/pipe_operator/prec_007.phpt new file mode 100644 index 0000000000000..c29db8565008f --- /dev/null +++ b/Zend/tests/pipe_operator/prec_007.phpt @@ -0,0 +1,24 @@ +--TEST-- +Pipe precedence 007 +--FILE-- + (fn ($x) => $x ?? throw new Exception('Value may not be null')) + |> (fn ($x) => var_dump($x)); + +$value = null; +$value + |> (fn ($x) => $x ?? throw new Exception('Value may not be null')) + |> (fn ($x) => var_dump($x)); + +?> +--EXPECTF-- +int(42) + +Fatal error: Uncaught Exception: Value may not be null in %s:%d +Stack trace: +#0 %s(%d): {closure:%s:%d}(NULL) +#1 {main} + thrown in %s on line %d diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 1172cba2d4f16..fd2526fb5e667 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2070,6 +2070,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio case ZEND_AST_ARROW_FUNC: case ZEND_AST_METHOD: decl = (const zend_ast_decl *) ast; + if (decl->kind == ZEND_AST_ARROW_FUNC && (decl->attr & ZEND_PARENTHESIZED_ARROW_FUNC)) { + smart_str_appendc(str, '('); + } if (decl->child[4]) { bool newlines = !(ast->kind == ZEND_AST_CLOSURE || ast->kind == ZEND_AST_ARROW_FUNC); zend_ast_export_attributes(str, decl->child[4], indent, newlines); @@ -2113,6 +2116,9 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio } smart_str_appends(str, " => "); zend_ast_export_ex(str, body, 0, indent); + if (decl->attr & ZEND_PARENTHESIZED_ARROW_FUNC) { + smart_str_appendc(str, ')'); + } break; } diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 2e561f225917a..8ce1c49f6bb0c 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -220,7 +220,7 @@ typedef struct _zend_ast_op_array { /* Separate structure for function and class declaration, as they need extra information. */ typedef struct _zend_ast_decl { zend_ast_kind kind; - zend_ast_attr attr; /* Unused - for structure compatibility */ + zend_ast_attr attr; uint32_t start_lineno; uint32_t end_lineno; uint32_t flags; diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index db611be49ab45..17a9cbe35261a 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6464,6 +6464,10 @@ static void zend_compile_pipe(znode *result, zend_ast *ast) zend_ast *operand_ast = ast->child[0]; zend_ast *callable_ast = ast->child[1]; + if (callable_ast->kind == ZEND_AST_ARROW_FUNC && !(callable_ast->attr & ZEND_PARENTHESIZED_ARROW_FUNC)) { + zend_error_noreturn(E_COMPILE_ERROR, "Arrow functions on the right hand side of |> must be parenthesized"); + } + /* Compile the left hand side down to a value first. */ znode operand_result; zend_compile_expr(&operand_result, operand_ast); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 84afd44341928..0234f77775b33 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1207,6 +1207,9 @@ static zend_always_inline bool zend_check_arg_send_type(const zend_function *zf, /* Used to distinguish (parent::$prop)::get() from parent hook call. */ #define ZEND_PARENTHESIZED_STATIC_PROP 1 +/* Used to disallow pipes with arrow functions that lead to confusing parse trees. */ +#define ZEND_PARENTHESIZED_ARROW_FUNC 1 + /* For "use" AST nodes and the seen symbol table */ #define ZEND_SYMBOL_CLASS (1<<0) #define ZEND_SYMBOL_FUNCTION (1<<1) diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 3f2817b26ec44..e4d61006fe12f 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -1348,6 +1348,7 @@ expr: | '(' expr ')' { $$ = $2; if ($$->kind == ZEND_AST_CONDITIONAL) $$->attr = ZEND_PARENTHESIZED_CONDITIONAL; + if ($$->kind == ZEND_AST_ARROW_FUNC) $$->attr = ZEND_PARENTHESIZED_ARROW_FUNC; } | new_dereferenceable { $$ = $1; } | new_non_dereferenceable { $$ = $1; }