diff --git a/Zend/tests/nullsafe_operator/parenthesized_short_circuit.phpt b/Zend/tests/nullsafe_operator/parenthesized_short_circuit.phpt new file mode 100644 index 0000000000000..5524f9e180fd1 --- /dev/null +++ b/Zend/tests/nullsafe_operator/parenthesized_short_circuit.phpt @@ -0,0 +1,55 @@ +--TEST-- +Parentheses should contain nullsafe short-circuiting +--FILE-- +bar()[0]); +var_dump($nullable?->prop[0]); +var_dump($foo?->bar()[0]); + +echo "\nWith parentheses (should warn on null):\n"; +var_dump(($nullable?->bar())[0]); +var_dump(($nullable?->prop)[0]); + +echo "\nDirect null (should warn):\n"; +var_dump((null)[0]); + +echo "\nNon-null with parentheses:\n"; +var_dump(($foo?->bar())[0]); +var_dump(($foo?->prop)[0]); + +?> +--EXPECTF-- +Without parentheses (short-circuits): +NULL +NULL +int(1) + +With parentheses (should warn on null): + +Warning: Trying to access array offset on null in %s on line %d +NULL + +Warning: Trying to access array offset on null in %s on line %d +NULL + +Direct null (should warn): + +Warning: Trying to access array offset on null in %s on line %d +NULL + +Non-null with parentheses: +int(1) +string(1) "v" diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 50ba8029873ad..f5e2a8c7c8173 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -2509,6 +2509,9 @@ static bool zend_ast_is_short_circuited(const zend_ast *ast) return zend_ast_is_short_circuited(ast->child[0]); case ZEND_AST_NULLSAFE_PROP: case ZEND_AST_NULLSAFE_METHOD_CALL: + if (ast->attr & ZEND_PARENTHESIZED_NULLSAFE) { + return 0; + } return 1; default: return 0; @@ -2530,6 +2533,9 @@ static void zend_assert_not_short_circuited(const zend_ast *ast) static void zend_short_circuiting_mark_inner(zend_ast *ast) { if (zend_ast_kind_is_short_circuited(ast->kind)) { + if (ast->attr & ZEND_PARENTHESIZED_NULLSAFE) { + return; + } ast->attr |= ZEND_SHORT_CIRCUITING_INNER; } } @@ -12120,6 +12126,11 @@ static zend_op *zend_delayed_compile_var(znode *result, zend_ast *ast, uint32_t { zend_check_stack_limit(); + if ((ast->kind == ZEND_AST_NULLSAFE_PROP || ast->kind == ZEND_AST_NULLSAFE_METHOD_CALL) + && (ast->attr & ZEND_PARENTHESIZED_NULLSAFE)) { + return zend_compile_var(result, ast, type, by_ref); + } + switch (ast->kind) { case ZEND_AST_VAR: return zend_compile_simple_var(result, ast, type, true); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 86fab4b57ded6..d95c9d71c8219 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -1224,6 +1224,9 @@ static zend_always_inline bool zend_check_arg_send_type(const zend_function *zf, /* Used to disallow pipes with arrow functions that lead to confusing parse trees. */ #define ZEND_PARENTHESIZED_ARROW_FUNC 1 +/* Used to contain nullsafe short-circuiting within parentheses */ +#define ZEND_PARENTHESIZED_NULLSAFE 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 e4d61006fe12f..4a8e5652e3f46 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -1349,6 +1349,8 @@ expr: $$ = $2; if ($$->kind == ZEND_AST_CONDITIONAL) $$->attr = ZEND_PARENTHESIZED_CONDITIONAL; if ($$->kind == ZEND_AST_ARROW_FUNC) $$->attr = ZEND_PARENTHESIZED_ARROW_FUNC; + if ($$->kind == ZEND_AST_NULLSAFE_PROP) $$->attr = ZEND_PARENTHESIZED_NULLSAFE; + if ($$->kind == ZEND_AST_NULLSAFE_METHOD_CALL) $$->attr = ZEND_PARENTHESIZED_NULLSAFE; } | new_dereferenceable { $$ = $1; } | new_non_dereferenceable { $$ = $1; } @@ -1544,6 +1546,8 @@ fully_dereferenceable: | '(' expr ')' { $$ = $2; if ($$->kind == ZEND_AST_STATIC_PROP) $$->attr = ZEND_PARENTHESIZED_STATIC_PROP; + if ($$->kind == ZEND_AST_NULLSAFE_PROP) $$->attr = ZEND_PARENTHESIZED_NULLSAFE; + if ($$->kind == ZEND_AST_NULLSAFE_METHOD_CALL) $$->attr = ZEND_PARENTHESIZED_NULLSAFE; } | dereferenceable_scalar { $$ = $1; } | class_constant { $$ = $1; } @@ -1557,7 +1561,11 @@ array_object_dereferenceable: callable_expr: callable_variable { $$ = $1; } - | '(' expr ')' { $$ = $2; } + | '(' expr ')' { + $$ = $2; + if ($$->kind == ZEND_AST_NULLSAFE_PROP) $$->attr = ZEND_PARENTHESIZED_NULLSAFE; + if ($$->kind == ZEND_AST_NULLSAFE_METHOD_CALL) $$->attr = ZEND_PARENTHESIZED_NULLSAFE; + } | dereferenceable_scalar { $$ = $1; } | new_dereferenceable { $$ = $1; } ;