diff --git a/UPGRADING b/UPGRADING index e2e1f7beb333c..625771ca6064d 100644 --- a/UPGRADING +++ b/UPGRADING @@ -78,6 +78,8 @@ PHP 8.2 UPGRADE NOTES RFC: https://wiki.php.net/rfc/dnf_types . Added error_log_mode ini setting that allows setting of permissions for error log file. + . Added support for fetching properties of enums in constant expressions. + RFC: https://wiki.php.net/rfc/fetch_property_in_const_expressions - Curl: diff --git a/Zend/tests/prop_const_expr/attributes.phpt b/Zend/tests/prop_const_expr/attributes.phpt new file mode 100644 index 0000000000000..96ebb9191443f --- /dev/null +++ b/Zend/tests/prop_const_expr/attributes.phpt @@ -0,0 +1,44 @@ +--TEST-- +Allow fetching properties in attributes +--EXTENSIONS-- +reflection +--FILE-- +name)] +#[Attr(A::B->value)] +#[Attr(A::B?->name)] +#[Attr(A::B?->value)] +class C {} + +foreach ((new ReflectionClass(C::class))->getAttributes() as $reflectionAttribute) { + var_dump($reflectionAttribute->newInstance()); +} + +?> +--EXPECT-- +object(Attr)#1 (1) { + ["value"]=> + string(1) "B" +} +object(Attr)#1 (1) { + ["value"]=> + string(1) "C" +} +object(Attr)#1 (1) { + ["value"]=> + string(1) "B" +} +object(Attr)#1 (1) { + ["value"]=> + string(1) "C" +} diff --git a/Zend/tests/prop_const_expr/basic.phpt b/Zend/tests/prop_const_expr/basic.phpt new file mode 100644 index 0000000000000..7499e7c55e9ee --- /dev/null +++ b/Zend/tests/prop_const_expr/basic.phpt @@ -0,0 +1,27 @@ +--TEST-- +Allow fetching properties in constant expressions on enums +--FILE-- +name; +const A_value = A::Case->value; + +var_dump(A_name); +var_dump(A_value); + +const A_name_nullsafe = A::Case?->name; +const A_value_nullsafe = A::Case?->value; + +var_dump(A_name_nullsafe); +var_dump(A_value_nullsafe); + +?> +--EXPECT-- +string(4) "Case" +string(7) "A::Case" +string(4) "Case" +string(7) "A::Case" diff --git a/Zend/tests/prop_const_expr/basic_nullsafe.phpt b/Zend/tests/prop_const_expr/basic_nullsafe.phpt new file mode 100644 index 0000000000000..da870c0328b0a --- /dev/null +++ b/Zend/tests/prop_const_expr/basic_nullsafe.phpt @@ -0,0 +1,46 @@ +--TEST-- +Nullsafe property constant expression +--FILE-- +test; +var_dump(A); + +const B = (null)?->test->test; +var_dump(B); + +const C = (null)->test?->test; +var_dump(C); + +const D = (null)?->test['test']; +var_dump(D); + +const E = (null)['test']?->test; +var_dump(E); + +const F = (null)?->{new Printer}; +var_dump(F); + +const G = (null)?->test + (new Printer ? 1 : 0); +var_dump(G); + +?> +--EXPECTF-- +NULL +NULL + +Warning: Attempt to read property "test" on null in %s on line %d +NULL +NULL + +Warning: Trying to access array offset on value of type null in %s on line %d +NULL +NULL +Printer +int(1) diff --git a/Zend/tests/prop_const_expr/class_const.phpt b/Zend/tests/prop_const_expr/class_const.phpt new file mode 100644 index 0000000000000..ff49ef58be2d4 --- /dev/null +++ b/Zend/tests/prop_const_expr/class_const.phpt @@ -0,0 +1,27 @@ +--TEST-- +Allow fetching properties in class constants +--FILE-- +name; + const A_value = A::Case->value; + const A_name_nullsafe = A::Case?->name; + const A_value_nullsafe = A::Case?->value; +} + +var_dump(C::A_name); +var_dump(C::A_value); +var_dump(C::A_name_nullsafe); +var_dump(C::A_value_nullsafe); + +?> +--EXPECT-- +string(4) "Case" +string(7) "A::Case" +string(4) "Case" +string(7) "A::Case" diff --git a/Zend/tests/prop_const_expr/default_args.phpt b/Zend/tests/prop_const_expr/default_args.phpt new file mode 100644 index 0000000000000..9046ddc82942c --- /dev/null +++ b/Zend/tests/prop_const_expr/default_args.phpt @@ -0,0 +1,34 @@ +--TEST-- +Property fetch in default argument +--FILE-- +name, + $value = A::B->value, + $nameNullsafe = A::B?->name, + $valueNullsafe = A::B?->value, +) { + var_dump($name); + var_dump($value); + var_dump($nameNullsafe); + var_dump($valueNullsafe); +} + +test(); +test('D', 'E', 'F', 'G'); + +?> +--EXPECT-- +string(1) "B" +string(1) "C" +string(1) "B" +string(1) "C" +string(1) "D" +string(1) "E" +string(1) "F" +string(1) "G" diff --git a/Zend/tests/prop_const_expr/enum_initializer.phpt b/Zend/tests/prop_const_expr/enum_initializer.phpt new file mode 100644 index 0000000000000..9b5c0314b69b1 --- /dev/null +++ b/Zend/tests/prop_const_expr/enum_initializer.phpt @@ -0,0 +1,30 @@ +--TEST-- +Property fetch in enum initializers +--FILE-- +name; + case F = A::B->value; +} + +enum G: string { + case H = A::B?->name; + case I = A::B?->value; +} + +var_dump(D::E->value); +var_dump(D::F->value); +var_dump(G::H->value); +var_dump(G::I->value); + +?> +--EXPECT-- +string(1) "B" +string(1) "C" +string(1) "B" +string(1) "C" diff --git a/Zend/tests/prop_const_expr/lhs_class_not_found.phpt b/Zend/tests/prop_const_expr/lhs_class_not_found.phpt new file mode 100644 index 0000000000000..9d10ec0f7b87c --- /dev/null +++ b/Zend/tests/prop_const_expr/lhs_class_not_found.phpt @@ -0,0 +1,13 @@ +--TEST-- +Property constant expression lhs error +--FILE-- +prop; + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Class "A" not found in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/prop_const_expr/lhs_class_not_found_nullsafe.phpt b/Zend/tests/prop_const_expr/lhs_class_not_found_nullsafe.phpt new file mode 100644 index 0000000000000..76bf62b2eda15 --- /dev/null +++ b/Zend/tests/prop_const_expr/lhs_class_not_found_nullsafe.phpt @@ -0,0 +1,13 @@ +--TEST-- +Nullsafe property constant expression lhs error +--FILE-- +prop; + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Class "A" not found in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/prop_const_expr/lhs_non_object.phpt b/Zend/tests/prop_const_expr/lhs_non_object.phpt new file mode 100644 index 0000000000000..357c58c3c0e30 --- /dev/null +++ b/Zend/tests/prop_const_expr/lhs_non_object.phpt @@ -0,0 +1,18 @@ +--TEST-- +Property constant expression lhs wrong type +--FILE-- +prop; +var_dump(A_prop); + +const A_prop_nullsafe = (42)?->prop; +var_dump(A_prop_nullsafe); + +?> +--EXPECTF-- +Warning: Attempt to read property "prop" on int in %s on line %d +NULL + +Warning: Attempt to read property "prop" on int in %s on line %d +NULL diff --git a/Zend/tests/prop_const_expr/non_enums.phpt b/Zend/tests/prop_const_expr/non_enums.phpt new file mode 100644 index 0000000000000..e045d4378fa06 --- /dev/null +++ b/Zend/tests/prop_const_expr/non_enums.phpt @@ -0,0 +1,17 @@ +--TEST-- +Disallow fetching properties in constant expressions on non-enums +--FILE-- +prop; + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Fetching properties on non-enums in constant expressions is not allowed in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/prop_const_expr/non_enums_catchable.phpt b/Zend/tests/prop_const_expr/non_enums_catchable.phpt new file mode 100644 index 0000000000000..6f410ac7acbe7 --- /dev/null +++ b/Zend/tests/prop_const_expr/non_enums_catchable.phpt @@ -0,0 +1,26 @@ +--TEST-- +RHS gets evaluated before throwing error when accessing properties on non-enums in constant expressions +--FILE-- +{new Printer ? 'printer' : null}; + +?> +--EXPECTF-- +Printer + +Fatal error: Uncaught Error: Fetching properties on non-enums in constant expressions is not allowed in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/prop_const_expr/non_enums_cost.phpt b/Zend/tests/prop_const_expr/non_enums_cost.phpt new file mode 100644 index 0000000000000..29311aa9ecaeb --- /dev/null +++ b/Zend/tests/prop_const_expr/non_enums_cost.phpt @@ -0,0 +1,18 @@ +--TEST-- +Disallow fetching properties in constant expressions on non-enums even if lhs is other const +--FILE-- +prop; + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Fetching properties on non-enums in constant expressions is not allowed in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/prop_const_expr/non_enums_nullsafe.phpt b/Zend/tests/prop_const_expr/non_enums_nullsafe.phpt new file mode 100644 index 0000000000000..428651c195e66 --- /dev/null +++ b/Zend/tests/prop_const_expr/non_enums_nullsafe.phpt @@ -0,0 +1,17 @@ +--TEST-- +Disallow nullsafe fetching properties in constant expressions on non-enums +--FILE-- +prop; + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Fetching properties on non-enums in constant expressions is not allowed in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/prop_const_expr/non_enums_rhs.phpt b/Zend/tests/prop_const_expr/non_enums_rhs.phpt new file mode 100644 index 0000000000000..0bffe9d8e02f5 --- /dev/null +++ b/Zend/tests/prop_const_expr/non_enums_rhs.phpt @@ -0,0 +1,26 @@ +--TEST-- +Error when fetching properties on non-enums in constant expressions is catchable +--FILE-- +prop) {} + +function test() { + try { + foo(); + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } +} + +test(); +test(); + +?> +--EXPECT-- +Fetching properties on non-enums in constant expressions is not allowed +Fetching properties on non-enums in constant expressions is not allowed diff --git a/Zend/tests/prop_const_expr/property_initializer.phpt b/Zend/tests/prop_const_expr/property_initializer.phpt new file mode 100644 index 0000000000000..3cfe5a87ffc65 --- /dev/null +++ b/Zend/tests/prop_const_expr/property_initializer.phpt @@ -0,0 +1,28 @@ +--TEST-- +Property fetch in property initializer +--FILE-- +name; + public int $e = A::B->value; + public string $f = A::B?->name; + public int $g = A::B?->value; +} + +$c = new C(); +var_dump($c->d); +var_dump($c->e); +var_dump($c->f); +var_dump($c->g); + +?> +--EXPECT-- +string(1) "B" +int(42) +string(1) "B" +int(42) diff --git a/Zend/tests/prop_const_expr/rhs_object.phpt b/Zend/tests/prop_const_expr/rhs_object.phpt new file mode 100644 index 0000000000000..d9d01b81cbe3c --- /dev/null +++ b/Zend/tests/prop_const_expr/rhs_object.phpt @@ -0,0 +1,18 @@ +--TEST-- +Property constant expression rhs wrong type +--FILE-- +{new B}; + +var_dump(A_prop); + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Object of class B could not be converted to string in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/prop_const_expr/rhs_object_nullsafe.phpt b/Zend/tests/prop_const_expr/rhs_object_nullsafe.phpt new file mode 100644 index 0000000000000..72242b16a42e5 --- /dev/null +++ b/Zend/tests/prop_const_expr/rhs_object_nullsafe.phpt @@ -0,0 +1,18 @@ +--TEST-- +Nullsafe property constant expression rhs wrong type +--FILE-- +{new B}; + +var_dump(A_prop); + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Object of class B could not be converted to string in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/prop_const_expr/rhs_prop_not_found.phpt b/Zend/tests/prop_const_expr/rhs_prop_not_found.phpt new file mode 100644 index 0000000000000..0994075902b27 --- /dev/null +++ b/Zend/tests/prop_const_expr/rhs_prop_not_found.phpt @@ -0,0 +1,22 @@ +--TEST-- +Property not found error +--FILE-- +prop; +var_dump(A_prop); + +const A_prop_nullsafe = A::B?->prop; +var_dump(A_prop_nullsafe); + +?> +--EXPECTF-- +Warning: Undefined property: A::$prop in %s on line %d +NULL + +Warning: Undefined property: A::$prop in %s on line %d +NULL diff --git a/Zend/tests/prop_const_expr/static_initalizer.phpt b/Zend/tests/prop_const_expr/static_initalizer.phpt new file mode 100644 index 0000000000000..837ddcd513da8 --- /dev/null +++ b/Zend/tests/prop_const_expr/static_initalizer.phpt @@ -0,0 +1,29 @@ +--TEST-- +Allow fetching properties in static initializer +--FILE-- +name; + static $value = A::Case->value; + static $nameNullsafe = A::Case?->name; + static $valueNullsafe = A::Case?->value; + + var_dump($name); + var_dump($value); + var_dump($nameNullsafe); + var_dump($valueNullsafe); +} + +foo(); + +?> +--EXPECT-- +string(4) "Case" +string(7) "A::Case" +string(4) "Case" +string(7) "A::Case" diff --git a/Zend/tests/prop_const_expr/static_property_initializer.phpt b/Zend/tests/prop_const_expr/static_property_initializer.phpt new file mode 100644 index 0000000000000..825c8d0907f3e --- /dev/null +++ b/Zend/tests/prop_const_expr/static_property_initializer.phpt @@ -0,0 +1,27 @@ +--TEST-- +Property fetch in static property initializer +--FILE-- +name; + public static int $e = A::B->value; + public static string $f = A::B?->name; + public static int $g = A::B?->value; +} + +var_dump(C::$d); +var_dump(C::$e); +var_dump(C::$f); +var_dump(C::$g); + +?> +--EXPECT-- +string(1) "B" +int(42) +string(1) "B" +int(42) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 6fa5e62dcd090..f366a3b7ae690 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -96,6 +96,14 @@ ZEND_API ZEND_COLD void zend_wrong_param_count(void) /* {{{ */ } /* }}} */ +ZEND_API ZEND_COLD void zend_wrong_property_read(zval *object, zval *property) +{ + zend_string *tmp_property_name; + zend_string *property_name = zval_get_tmp_string(property, &tmp_property_name); + zend_error(E_WARNING, "Attempt to read property \"%s\" on %s", ZSTR_VAL(property_name), zend_zval_type_name(object)); + zend_tmp_string_release(tmp_property_name); +} + /* Argument parsing API -- andrei */ ZEND_API const char *zend_get_type_by_const(int type) /* {{{ */ { diff --git a/Zend/zend_API.h b/Zend/zend_API.h index af844d0a87ba7..bec8bd86590e0 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -391,6 +391,7 @@ ZEND_API void zend_disable_functions(const char *function_list); ZEND_API zend_result zend_disable_class(const char *class_name, size_t class_name_length); ZEND_API ZEND_COLD void zend_wrong_param_count(void); +ZEND_API ZEND_COLD void zend_wrong_property_read(zval *object, zval *property); #define IS_CALLABLE_CHECK_SYNTAX_ONLY (1<<0) #define IS_CALLABLE_SUPPRESS_DEPRECATIONS (1<<1) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 2c23421f9f3ef..6bdf0c4e38883 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -498,10 +498,11 @@ zend_class_entry *zend_ast_fetch_class(zend_ast *ast, zend_class_entry *scope) return zend_fetch_class_with_scope(zend_ast_get_str(ast), ast->attr | ZEND_FETCH_CLASS_EXCEPTION, scope); } -ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast, zend_class_entry *scope) +static zend_result ZEND_FASTCALL zend_ast_evaluate_ex(zval *result, zend_ast *ast, zend_class_entry *scope, bool *short_circuited_ptr) { zval op1, op2; zend_result ret = SUCCESS; + *short_circuited_ptr = false; switch (ast->kind) { case ZEND_AST_BINARY_OP: @@ -733,10 +734,16 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast zend_error_noreturn(E_COMPILE_ERROR, "Cannot use [] for reading"); } - if (UNEXPECTED(zend_ast_evaluate(&op1, ast->child[0], scope) != SUCCESS)) { + bool short_circuited; + if (UNEXPECTED(zend_ast_evaluate_ex(&op1, ast->child[0], scope, &short_circuited) != SUCCESS)) { ret = FAILURE; break; } + if (short_circuited) { + *short_circuited_ptr = true; + ZVAL_NULL(result); + return SUCCESS; + } // DIM on objects is disallowed because it allows executing arbitrary expressions if (Z_TYPE(op1) == IS_OBJECT) { @@ -907,6 +914,68 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast } return SUCCESS; } + case ZEND_AST_PROP: + case ZEND_AST_NULLSAFE_PROP: + { + bool short_circuited; + if (UNEXPECTED(zend_ast_evaluate_ex(&op1, ast->child[0], scope, &short_circuited) != SUCCESS)) { + return FAILURE; + } + if (short_circuited) { + *short_circuited_ptr = true; + ZVAL_NULL(result); + return SUCCESS; + } + if (ast->kind == ZEND_AST_NULLSAFE_PROP && Z_TYPE(op1) == IS_NULL) { + *short_circuited_ptr = true; + ZVAL_NULL(result); + return SUCCESS; + } + + if (UNEXPECTED(zend_ast_evaluate(&op2, ast->child[1], scope) != SUCCESS)) { + zval_ptr_dtor_nogc(&op1); + return FAILURE; + } + + if (!try_convert_to_string(&op2)) { + zval_ptr_dtor_nogc(&op1); + zval_ptr_dtor_nogc(&op2); + return FAILURE; + } + + if (Z_TYPE(op1) != IS_OBJECT) { + zend_wrong_property_read(&op1, &op2); + + zval_ptr_dtor_nogc(&op1); + zval_ptr_dtor_nogc(&op2); + + ZVAL_NULL(result); + return SUCCESS; + } + + zend_object *zobj = Z_OBJ(op1); + if (!(zobj->ce->ce_flags & ZEND_ACC_ENUM)) { + zend_throw_error(NULL, "Fetching properties on non-enums in constant expressions is not allowed"); + zval_ptr_dtor_nogc(&op1); + zval_ptr_dtor_nogc(&op2); + return FAILURE; + } + + zend_string *name = Z_STR(op2); + zval *property_result = zend_read_property_ex(scope, zobj, name, 0, result); + if (EG(exception)) { + zval_ptr_dtor_nogc(&op1); + zval_ptr_dtor_nogc(&op2); + return FAILURE; + } + + if (result != property_result) { + ZVAL_COPY(result, property_result); + } + zval_ptr_dtor_nogc(&op1); + zval_ptr_dtor_nogc(&op2); + return SUCCESS; + } default: zend_throw_error(NULL, "Unsupported constant expression"); ret = FAILURE; @@ -914,6 +983,12 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast return ret; } +ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast, zend_class_entry *scope) +{ + bool short_circuited; + return zend_ast_evaluate_ex(result, ast, scope, &short_circuited); +} + static size_t ZEND_FASTCALL zend_ast_tree_size(zend_ast *ast) { size_t size; diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 85852f3c9cb9f..7dd3d598df10a 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9751,7 +9751,8 @@ static bool zend_is_allowed_in_const_expr(zend_ast_kind kind) /* {{{ */ || kind == ZEND_AST_MAGIC_CONST || kind == ZEND_AST_COALESCE || kind == ZEND_AST_CONST_ENUM_INIT || kind == ZEND_AST_NEW || kind == ZEND_AST_ARG_LIST - || kind == ZEND_AST_NAMED_ARG; + || kind == ZEND_AST_NAMED_ARG + || kind == ZEND_AST_PROP || kind == ZEND_AST_NULLSAFE_PROP; } /* }}} */ diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 46f88a1dbe6b4..adc875e2a7736 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1623,14 +1623,6 @@ ZEND_API ZEND_COLD void zend_wrong_string_offset_error(void) zend_throw_error(NULL, "%s", msg); } -static zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_wrong_property_read(zval *object, zval *property) -{ - zend_string *tmp_property_name; - zend_string *property_name = zval_get_tmp_string(property, &tmp_property_name); - zend_error(E_WARNING, "Attempt to read property \"%s\" on %s", ZSTR_VAL(property_name), zend_zval_type_name(object)); - zend_tmp_string_release(tmp_property_name); -} - ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_function(const zend_function *fbc) { if (fbc->common.scope) {