From 0d83f7a54bcb1e7e87eec0834158f92191dfee1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 20 Mar 2025 10:43:14 +0100 Subject: [PATCH 01/11] Add `#[\NoDiscard]` attribute definition --- .../nodiscard/property_readonly_001.phpt | 14 ++++++++ .../nodiscard/property_readonly_002.phpt | 15 +++++++++ Zend/zend_attributes.c | 27 +++++++++++++++ Zend/zend_attributes.h | 1 + Zend/zend_attributes.stub.php | 11 +++++++ Zend/zend_attributes_arginfo.h | 33 ++++++++++++++++++- 6 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/attributes/nodiscard/property_readonly_001.phpt create mode 100644 Zend/tests/attributes/nodiscard/property_readonly_002.phpt diff --git a/Zend/tests/attributes/nodiscard/property_readonly_001.phpt b/Zend/tests/attributes/nodiscard/property_readonly_001.phpt new file mode 100644 index 0000000000000..6f9779023126e --- /dev/null +++ b/Zend/tests/attributes/nodiscard/property_readonly_001.phpt @@ -0,0 +1,14 @@ +--TEST-- +#[\NoDiscard]: NoDiscard::$message is readonly. +--FILE-- +message = 'bar'; + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Cannot modify readonly property NoDiscard::$message in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/property_readonly_002.phpt b/Zend/tests/attributes/nodiscard/property_readonly_002.phpt new file mode 100644 index 0000000000000..f01594ca4b26c --- /dev/null +++ b/Zend/tests/attributes/nodiscard/property_readonly_002.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\NoDiscard]: __construct() respects that properties are readonly. +--FILE-- +__construct("bar"); + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Cannot modify readonly property NoDiscard::$message in %s:%d +Stack trace: +#0 %s(%d): NoDiscard->__construct('bar') +#1 {main} + thrown in %s on line %d diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index d7bcb1f54e889..48f79c12610f6 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -31,6 +31,7 @@ ZEND_API zend_class_entry *zend_ce_sensitive_parameter; ZEND_API zend_class_entry *zend_ce_sensitive_parameter_value; ZEND_API zend_class_entry *zend_ce_override; ZEND_API zend_class_entry *zend_ce_deprecated; +ZEND_API zend_class_entry *zend_ce_nodiscard; static zend_object_handlers attributes_object_handlers_sensitive_parameter_value; @@ -193,6 +194,29 @@ ZEND_METHOD(Deprecated, __construct) } } +ZEND_METHOD(NoDiscard, __construct) +{ + zend_string *message = NULL; + zval value; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_STR_OR_NULL(message) + ZEND_PARSE_PARAMETERS_END(); + + if (message) { + ZVAL_STR(&value, message); + } else { + ZVAL_NULL(&value); + } + zend_update_property_ex(zend_ce_nodiscard, Z_OBJ_P(ZEND_THIS), ZSTR_KNOWN(ZEND_STR_MESSAGE), &value); + + /* The assignment might fail due to 'readonly'. */ + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } +} + static zend_attribute *get_attribute(HashTable *attributes, zend_string *lcname, uint32_t offset) { if (attributes) { @@ -520,6 +544,9 @@ void zend_register_attribute_ce(void) zend_ce_deprecated = register_class_Deprecated(); attr = zend_mark_internal_attribute(zend_ce_deprecated); + + zend_ce_nodiscard = register_class_NoDiscard(); + attr = zend_mark_internal_attribute(zend_ce_nodiscard); } void zend_attributes_shutdown(void) diff --git a/Zend/zend_attributes.h b/Zend/zend_attributes.h index 8a825247c00f8..468488800ebf8 100644 --- a/Zend/zend_attributes.h +++ b/Zend/zend_attributes.h @@ -47,6 +47,7 @@ extern ZEND_API zend_class_entry *zend_ce_sensitive_parameter; extern ZEND_API zend_class_entry *zend_ce_sensitive_parameter_value; extern ZEND_API zend_class_entry *zend_ce_override; extern ZEND_API zend_class_entry *zend_ce_deprecated; +extern ZEND_API zend_class_entry *zend_ce_nodiscard; typedef struct { zend_string *name; diff --git a/Zend/zend_attributes.stub.php b/Zend/zend_attributes.stub.php index 0a35b0c57cb44..6351ccd771838 100644 --- a/Zend/zend_attributes.stub.php +++ b/Zend/zend_attributes.stub.php @@ -84,3 +84,14 @@ final class Deprecated public function __construct(?string $message = null, ?string $since = null) {} } + +/** + * @strict-properties + */ +#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)] +final class NoDiscard +{ + public readonly ?string $message; + + public function __construct(?string $message = null) {} +} diff --git a/Zend/zend_attributes_arginfo.h b/Zend/zend_attributes_arginfo.h index 018caa47d0ac5..aecb216291071 100644 --- a/Zend/zend_attributes_arginfo.h +++ b/Zend/zend_attributes_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 2358a0d820edd06a1702c84104bfd545af08311c */ + * Stub hash: 6b54bc195be211caabb395b621380681953c1f5a */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Attribute___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "Attribute::TARGET_ALL") @@ -29,6 +29,10 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Deprecated___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, since, IS_STRING, 1, "null") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_NoDiscard___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, message, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + ZEND_METHOD(Attribute, __construct); ZEND_METHOD(ReturnTypeWillChange, __construct); ZEND_METHOD(AllowDynamicProperties, __construct); @@ -38,6 +42,7 @@ ZEND_METHOD(SensitiveParameterValue, getValue); ZEND_METHOD(SensitiveParameterValue, __debugInfo); ZEND_METHOD(Override, __construct); ZEND_METHOD(Deprecated, __construct); +ZEND_METHOD(NoDiscard, __construct); static const zend_function_entry class_Attribute_methods[] = { ZEND_ME(Attribute, __construct, arginfo_class_Attribute___construct, ZEND_ACC_PUBLIC) @@ -76,6 +81,11 @@ static const zend_function_entry class_Deprecated_methods[] = { ZEND_FE_END }; +static const zend_function_entry class_NoDiscard_methods[] = { + ZEND_ME(NoDiscard, __construct, arginfo_class_NoDiscard___construct, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static zend_class_entry *register_class_Attribute(void) { zend_class_entry ce, *class_entry; @@ -253,3 +263,24 @@ static zend_class_entry *register_class_Deprecated(void) return class_entry; } + +static zend_class_entry *register_class_NoDiscard(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "NoDiscard", class_NoDiscard_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES); + + zval property_message_default_value; + ZVAL_UNDEF(&property_message_default_value); + zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_MESSAGE), &property_message_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING|MAY_BE_NULL)); + + zend_string *attribute_name_Attribute_class_NoDiscard_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, 1); + zend_attribute *attribute_Attribute_class_NoDiscard_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_NoDiscard_0, 1); + zend_string_release(attribute_name_Attribute_class_NoDiscard_0); + zval attribute_Attribute_class_NoDiscard_0_arg0; + ZVAL_LONG(&attribute_Attribute_class_NoDiscard_0_arg0, ZEND_ATTRIBUTE_TARGET_METHOD | ZEND_ATTRIBUTE_TARGET_FUNCTION); + ZVAL_COPY_VALUE(&attribute_Attribute_class_NoDiscard_0->args[0].value, &attribute_Attribute_class_NoDiscard_0_arg0); + + return class_entry; +} From 6f7aec528aa0f3afede7beb7c4c3a60b406037dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 20 Mar 2025 10:50:02 +0100 Subject: [PATCH 02/11] Apply `#[\NoDiscard]` to `\DateTimeImmutable::set*()` --- ext/date/php_date.stub.php | 9 ++++ ext/date/php_date_arginfo.h | 84 ++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/ext/date/php_date.stub.php b/ext/date/php_date.stub.php index d0119aac88ad5..f375c60ff0a4c 100644 --- a/ext/date/php_date.stub.php +++ b/ext/date/php_date.stub.php @@ -524,29 +524,38 @@ public function getMicrosecond(): int {} public function diff(DateTimeInterface $targetObject, bool $absolute = false): DateInterval {} /** @tentative-return-type */ + #[\NoDiscard(message: "as DateTimeImmutable::modify() does not modify the object itself")] public function modify(string $modifier): DateTimeImmutable {} /** @tentative-return-type */ + #[\NoDiscard(message: "as DateTimeImmutable::add() does not modify the object itself")] public function add(DateInterval $interval): DateTimeImmutable {} /** @tentative-return-type */ + #[\NoDiscard(message: "as DateTimeImmutable::sub() does not modify the object itself")] public function sub(DateInterval $interval): DateTimeImmutable {} /** @tentative-return-type */ + #[\NoDiscard(message: "as DateTimeImmutable::setTimezone() does not modify the object itself")] public function setTimezone(DateTimeZone $timezone): DateTimeImmutable {} /** @tentative-return-type */ + #[\NoDiscard(message: "as DateTimeImmutable::setTime() does not modify the object itself")] public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): DateTimeImmutable {} /** @tentative-return-type */ + #[\NoDiscard(message: "as DateTimeImmutable::setDate() does not modify the object itself")] public function setDate(int $year, int $month, int $day): DateTimeImmutable {} /** @tentative-return-type */ + #[\NoDiscard(message: "as DateTimeImmutable::setISODate() does not modify the object itself")] public function setISODate(int $year, int $week, int $dayOfWeek = 1): DateTimeImmutable {} /** @tentative-return-type */ + #[\NoDiscard(message: "as DateTimeImmutable::setTimestamp() does not modify the object itself")] public function setTimestamp(int $timestamp): DateTimeImmutable {} + #[\NoDiscard(message: "as DateTimeImmutable::setMicrosecond() does not modify the object itself")] public function setMicrosecond(int $microsecond): static {} /** @tentative-return-type */ diff --git a/ext/date/php_date_arginfo.h b/ext/date/php_date_arginfo.h index 8ce0114206cfe..a808a8c38c56d 100644 --- a/ext/date/php_date_arginfo.h +++ b/ext/date/php_date_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: d7a318f6fd85e23c6352323e03c323035a511738 */ + * Stub hash: 093743b4fe7a698d1262cc1a81b60a85064fdccb */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_strtotime, 0, 1, MAY_BE_LONG|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, datetime, IS_STRING, 0) @@ -989,6 +989,88 @@ static zend_class_entry *register_class_DateTimeImmutable(zend_class_entry *clas class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0); zend_class_implements(class_entry, 1, class_entry_DateTimeInterface); + + zend_string *attribute_name_NoDiscard_func_modify_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_modify_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "modify", sizeof("modify") - 1), attribute_name_NoDiscard_func_modify_0, 1); + zend_string_release(attribute_name_NoDiscard_func_modify_0); + zval attribute_NoDiscard_func_modify_0_arg0; + zend_string *attribute_NoDiscard_func_modify_0_arg0_str = zend_string_init("as DateTimeImmutable::modify() does not modify the object itself", strlen("as DateTimeImmutable::modify() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_modify_0_arg0, attribute_NoDiscard_func_modify_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_modify_0->args[0].value, &attribute_NoDiscard_func_modify_0_arg0); + attribute_NoDiscard_func_modify_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_add_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_add_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "add", sizeof("add") - 1), attribute_name_NoDiscard_func_add_0, 1); + zend_string_release(attribute_name_NoDiscard_func_add_0); + zval attribute_NoDiscard_func_add_0_arg0; + zend_string *attribute_NoDiscard_func_add_0_arg0_str = zend_string_init("as DateTimeImmutable::add() does not modify the object itself", strlen("as DateTimeImmutable::add() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_add_0_arg0, attribute_NoDiscard_func_add_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_add_0->args[0].value, &attribute_NoDiscard_func_add_0_arg0); + attribute_NoDiscard_func_add_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_sub_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_sub_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "sub", sizeof("sub") - 1), attribute_name_NoDiscard_func_sub_0, 1); + zend_string_release(attribute_name_NoDiscard_func_sub_0); + zval attribute_NoDiscard_func_sub_0_arg0; + zend_string *attribute_NoDiscard_func_sub_0_arg0_str = zend_string_init("as DateTimeImmutable::sub() does not modify the object itself", strlen("as DateTimeImmutable::sub() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_sub_0_arg0, attribute_NoDiscard_func_sub_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_sub_0->args[0].value, &attribute_NoDiscard_func_sub_0_arg0); + attribute_NoDiscard_func_sub_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_settimezone_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_settimezone_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "settimezone", sizeof("settimezone") - 1), attribute_name_NoDiscard_func_settimezone_0, 1); + zend_string_release(attribute_name_NoDiscard_func_settimezone_0); + zval attribute_NoDiscard_func_settimezone_0_arg0; + zend_string *attribute_NoDiscard_func_settimezone_0_arg0_str = zend_string_init("as DateTimeImmutable::setTimezone() does not modify the object itself", strlen("as DateTimeImmutable::setTimezone() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_settimezone_0_arg0, attribute_NoDiscard_func_settimezone_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_settimezone_0->args[0].value, &attribute_NoDiscard_func_settimezone_0_arg0); + attribute_NoDiscard_func_settimezone_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_settime_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_settime_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "settime", sizeof("settime") - 1), attribute_name_NoDiscard_func_settime_0, 1); + zend_string_release(attribute_name_NoDiscard_func_settime_0); + zval attribute_NoDiscard_func_settime_0_arg0; + zend_string *attribute_NoDiscard_func_settime_0_arg0_str = zend_string_init("as DateTimeImmutable::setTime() does not modify the object itself", strlen("as DateTimeImmutable::setTime() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_settime_0_arg0, attribute_NoDiscard_func_settime_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_settime_0->args[0].value, &attribute_NoDiscard_func_settime_0_arg0); + attribute_NoDiscard_func_settime_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_setdate_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_setdate_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "setdate", sizeof("setdate") - 1), attribute_name_NoDiscard_func_setdate_0, 1); + zend_string_release(attribute_name_NoDiscard_func_setdate_0); + zval attribute_NoDiscard_func_setdate_0_arg0; + zend_string *attribute_NoDiscard_func_setdate_0_arg0_str = zend_string_init("as DateTimeImmutable::setDate() does not modify the object itself", strlen("as DateTimeImmutable::setDate() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_setdate_0_arg0, attribute_NoDiscard_func_setdate_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_setdate_0->args[0].value, &attribute_NoDiscard_func_setdate_0_arg0); + attribute_NoDiscard_func_setdate_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_setisodate_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_setisodate_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "setisodate", sizeof("setisodate") - 1), attribute_name_NoDiscard_func_setisodate_0, 1); + zend_string_release(attribute_name_NoDiscard_func_setisodate_0); + zval attribute_NoDiscard_func_setisodate_0_arg0; + zend_string *attribute_NoDiscard_func_setisodate_0_arg0_str = zend_string_init("as DateTimeImmutable::setISODate() does not modify the object itself", strlen("as DateTimeImmutable::setISODate() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_setisodate_0_arg0, attribute_NoDiscard_func_setisodate_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_setisodate_0->args[0].value, &attribute_NoDiscard_func_setisodate_0_arg0); + attribute_NoDiscard_func_setisodate_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_settimestamp_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_settimestamp_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "settimestamp", sizeof("settimestamp") - 1), attribute_name_NoDiscard_func_settimestamp_0, 1); + zend_string_release(attribute_name_NoDiscard_func_settimestamp_0); + zval attribute_NoDiscard_func_settimestamp_0_arg0; + zend_string *attribute_NoDiscard_func_settimestamp_0_arg0_str = zend_string_init("as DateTimeImmutable::setTimestamp() does not modify the object itself", strlen("as DateTimeImmutable::setTimestamp() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_settimestamp_0_arg0, attribute_NoDiscard_func_settimestamp_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_settimestamp_0->args[0].value, &attribute_NoDiscard_func_settimestamp_0_arg0); + attribute_NoDiscard_func_settimestamp_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_setmicrosecond_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_setmicrosecond_0 = zend_add_function_attribute(zend_hash_str_find_ptr(&class_entry->function_table, "setmicrosecond", sizeof("setmicrosecond") - 1), attribute_name_NoDiscard_func_setmicrosecond_0, 1); + zend_string_release(attribute_name_NoDiscard_func_setmicrosecond_0); + zval attribute_NoDiscard_func_setmicrosecond_0_arg0; + zend_string *attribute_NoDiscard_func_setmicrosecond_0_arg0_str = zend_string_init("as DateTimeImmutable::setMicrosecond() does not modify the object itself", strlen("as DateTimeImmutable::setMicrosecond() does not modify the object itself"), 1); + ZVAL_STR(&attribute_NoDiscard_func_setmicrosecond_0_arg0, attribute_NoDiscard_func_setmicrosecond_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_setmicrosecond_0->args[0].value, &attribute_NoDiscard_func_setmicrosecond_0_arg0); + attribute_NoDiscard_func_setmicrosecond_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + return class_entry; } From 5fcdd24519aa239bf3892336d1f054d9d406e350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 20 Mar 2025 10:50:26 +0100 Subject: [PATCH 03/11] Apply `#[\NoDiscard]` to `flock()` --- ext/standard/basic_functions.stub.php | 1 + ext/standard/basic_functions_arginfo.h | 11 ++++++++++- ext/standard/tests/file/userstreams_004.phpt | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index e7f4ff8844714..b6533d8dc20e8 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -2732,6 +2732,7 @@ function proc_nice(int $priority): bool {} * @param resource $stream * @param int $would_block */ +#[\NoDiscard(message: "as locking the stream might have failed")] function flock($stream, int $operation, &$would_block = null): bool {} /** diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 3d92288643159..d0fb331e190fc 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 85677dc3476d25b7820fd3a26fe39f2e9378b6e7 */ + * Stub hash: 824ccb41163307bd0fad452b705a8222b6f42d09 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -4039,6 +4039,15 @@ static void register_basic_functions_symbols(int module_number) ZVAL_COPY_VALUE(&attribute_Deprecated_func_utf8_decode_0->args[1].value, &attribute_Deprecated_func_utf8_decode_0_arg1); attribute_Deprecated_func_utf8_decode_0->args[1].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + zend_string *attribute_name_NoDiscard_func_flock_0 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_flock_0 = zend_add_function_attribute(zend_hash_str_find_ptr(CG(function_table), "flock", sizeof("flock") - 1), attribute_name_NoDiscard_func_flock_0, 1); + zend_string_release(attribute_name_NoDiscard_func_flock_0); + zval attribute_NoDiscard_func_flock_0_arg0; + zend_string *attribute_NoDiscard_func_flock_0_arg0_str = zend_string_init("as locking the stream might have failed", strlen("as locking the stream might have failed"), 1); + ZVAL_STR(&attribute_NoDiscard_func_flock_0_arg0, attribute_NoDiscard_func_flock_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_flock_0->args[0].value, &attribute_NoDiscard_func_flock_0_arg0); + attribute_NoDiscard_func_flock_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + zend_add_parameter_attribute(zend_hash_str_find_ptr(CG(function_table), "password_hash", sizeof("password_hash") - 1), 0, ZSTR_KNOWN(ZEND_STR_SENSITIVEPARAMETER), 0); zend_add_parameter_attribute(zend_hash_str_find_ptr(CG(function_table), "password_verify", sizeof("password_verify") - 1), 0, ZSTR_KNOWN(ZEND_STR_SENSITIVEPARAMETER), 0); diff --git a/ext/standard/tests/file/userstreams_004.phpt b/ext/standard/tests/file/userstreams_004.phpt index 959f02f5b1cd7..fa68e5aa9307a 100644 --- a/ext/standard/tests/file/userstreams_004.phpt +++ b/ext/standard/tests/file/userstreams_004.phpt @@ -19,7 +19,7 @@ class test_wrapper extends test_wrapper_base { } function test($name, $fd, $mode) { echo "------ $name: -------\n"; - flock($fd, $mode); + (void)flock($fd, $mode); $data = stream_get_meta_data($fd); var_dump($data['wrapper_data']->mode === $mode); } From c9c7e0864632da616994d78d74c409b40802da6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 20 Mar 2025 10:52:00 +0100 Subject: [PATCH 04/11] Add `ZEND_ACC_NODISCARD` flag --- Zend/zend_compile.c | 10 ++++++++++ Zend/zend_compile.h | 5 ++++- build/gen_stub.php | 10 +++++++--- ext/date/php_date_arginfo.h | 18 +++++++++--------- ext/standard/basic_functions_arginfo.h | 2 +- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 8e4221673c4cf..db7e759c8f1a6 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8353,6 +8353,16 @@ static zend_op_array *zend_compile_func_decl_ex( if (deprecated_attribute) { op_array->fn_flags |= ZEND_ACC_DEPRECATED; } + + zend_attribute *nodiscard_attribute = zend_get_attribute_str( + op_array->attributes, + "nodiscard", + sizeof("nodiscard")-1 + ); + + if (nodiscard_attribute) { + op_array->fn_flags |= ZEND_ACC_NODISCARD; + } } /* Do not leak the class scope into free standing functions, even if they are dynamically diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 224a68be749cb..cae695fe7d7ec 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -333,7 +333,7 @@ typedef struct _zend_oparray_context { /* Class cannot be serialized or unserialized | | | */ #define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */ /* | | | */ -/* Function Flags (unused: 29-30) | | | */ +/* Function Flags (unused: 30) | | | */ /* ============== | | | */ /* | | | */ /* deprecation flag | | | */ @@ -395,6 +395,9 @@ typedef struct _zend_oparray_context { /* has #[\Override] attribute | | | */ #define ZEND_ACC_OVERRIDE (1 << 28) /* | X | | */ /* | | | */ +/* has #[\NoDiscard] attribute | | | */ +#define ZEND_ACC_NODISCARD (1 << 29) /* | X | | */ +/* | | | */ /* op_array uses strict mode types | | | */ #define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */ diff --git a/build/gen_stub.php b/build/gen_stub.php index f8e16064ee292..e69846733a9d9 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -1519,9 +1519,13 @@ private function getArginfoFlagsByPhpVersions(): array } foreach ($this->attributes as $attr) { - if ($attr->class === "Deprecated") { - $flags[] = "ZEND_ACC_DEPRECATED"; - break; + switch ($attr->class) { + case "Deprecated": + $flags[] = "ZEND_ACC_DEPRECATED"; + break; + case "NoDiscard": + $flags[] = "ZEND_ACC_NODISCARD"; + break; } } diff --git a/ext/date/php_date_arginfo.h b/ext/date/php_date_arginfo.h index a808a8c38c56d..82e9f0f1718ae 100644 --- a/ext/date/php_date_arginfo.h +++ b/ext/date/php_date_arginfo.h @@ -725,15 +725,15 @@ static const zend_function_entry class_DateTimeImmutable_methods[] = { ZEND_RAW_FENTRY("getTimestamp", zif_date_timestamp_get, arginfo_class_DateTimeImmutable_getTimestamp, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_RAW_FENTRY("getMicrosecond", zim_DateTime_getMicrosecond, arginfo_class_DateTimeImmutable_getMicrosecond, ZEND_ACC_PUBLIC, NULL, NULL) ZEND_RAW_FENTRY("diff", zif_date_diff, arginfo_class_DateTimeImmutable_diff, ZEND_ACC_PUBLIC, NULL, NULL) - ZEND_ME(DateTimeImmutable, modify, arginfo_class_DateTimeImmutable_modify, ZEND_ACC_PUBLIC) - ZEND_ME(DateTimeImmutable, add, arginfo_class_DateTimeImmutable_add, ZEND_ACC_PUBLIC) - ZEND_ME(DateTimeImmutable, sub, arginfo_class_DateTimeImmutable_sub, ZEND_ACC_PUBLIC) - ZEND_ME(DateTimeImmutable, setTimezone, arginfo_class_DateTimeImmutable_setTimezone, ZEND_ACC_PUBLIC) - ZEND_ME(DateTimeImmutable, setTime, arginfo_class_DateTimeImmutable_setTime, ZEND_ACC_PUBLIC) - ZEND_ME(DateTimeImmutable, setDate, arginfo_class_DateTimeImmutable_setDate, ZEND_ACC_PUBLIC) - ZEND_ME(DateTimeImmutable, setISODate, arginfo_class_DateTimeImmutable_setISODate, ZEND_ACC_PUBLIC) - ZEND_ME(DateTimeImmutable, setTimestamp, arginfo_class_DateTimeImmutable_setTimestamp, ZEND_ACC_PUBLIC) - ZEND_ME(DateTimeImmutable, setMicrosecond, arginfo_class_DateTimeImmutable_setMicrosecond, ZEND_ACC_PUBLIC) + ZEND_ME(DateTimeImmutable, modify, arginfo_class_DateTimeImmutable_modify, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) + ZEND_ME(DateTimeImmutable, add, arginfo_class_DateTimeImmutable_add, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) + ZEND_ME(DateTimeImmutable, sub, arginfo_class_DateTimeImmutable_sub, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) + ZEND_ME(DateTimeImmutable, setTimezone, arginfo_class_DateTimeImmutable_setTimezone, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) + ZEND_ME(DateTimeImmutable, setTime, arginfo_class_DateTimeImmutable_setTime, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) + ZEND_ME(DateTimeImmutable, setDate, arginfo_class_DateTimeImmutable_setDate, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) + ZEND_ME(DateTimeImmutable, setISODate, arginfo_class_DateTimeImmutable_setISODate, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) + ZEND_ME(DateTimeImmutable, setTimestamp, arginfo_class_DateTimeImmutable_setTimestamp, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) + ZEND_ME(DateTimeImmutable, setMicrosecond, arginfo_class_DateTimeImmutable_setMicrosecond, ZEND_ACC_PUBLIC|ZEND_ACC_NODISCARD) ZEND_ME(DateTimeImmutable, createFromMutable, arginfo_class_DateTimeImmutable_createFromMutable, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(DateTimeImmutable, createFromInterface, arginfo_class_DateTimeImmutable_createFromInterface, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_FE_END diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index d0fb331e190fc..d35377c900735 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -3205,7 +3205,7 @@ static const zend_function_entry ext_functions[] = { #if defined(HAVE_NICE) ZEND_FE(proc_nice, arginfo_proc_nice) #endif - ZEND_FE(flock, arginfo_flock) + ZEND_RAW_FENTRY("flock", zif_flock, arginfo_flock, ZEND_ACC_NODISCARD, NULL, NULL) ZEND_FE(get_meta_tags, arginfo_get_meta_tags) ZEND_FE(pclose, arginfo_pclose) ZEND_FE(popen, arginfo_popen) From a9db17344c64a69193aba6fb76444e9b76ee6a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 20 Mar 2025 10:57:14 +0100 Subject: [PATCH 05/11] Add compile-time restrictions on `#[\NoDiscard]` usage --- .../nodiscard/unsupported_clone.phpt | 16 ++++++++++++++ .../nodiscard/unsupported_constructor.phpt | 16 ++++++++++++++ .../nodiscard/unsupported_never_function.phpt | 15 +++++++++++++ .../unsupported_property_hook_get.phpt | 20 ++++++++++++++++++ .../unsupported_property_hook_set.phpt | 20 ++++++++++++++++++ .../nodiscard/unsupported_void_function.phpt | 15 +++++++++++++ Zend/zend_API.c | 10 +++++++++ Zend/zend_compile.c | 21 +++++++++++++++++++ 8 files changed, 133 insertions(+) create mode 100644 Zend/tests/attributes/nodiscard/unsupported_clone.phpt create mode 100644 Zend/tests/attributes/nodiscard/unsupported_constructor.phpt create mode 100644 Zend/tests/attributes/nodiscard/unsupported_never_function.phpt create mode 100644 Zend/tests/attributes/nodiscard/unsupported_property_hook_get.phpt create mode 100644 Zend/tests/attributes/nodiscard/unsupported_property_hook_set.phpt create mode 100644 Zend/tests/attributes/nodiscard/unsupported_void_function.phpt diff --git a/Zend/tests/attributes/nodiscard/unsupported_clone.phpt b/Zend/tests/attributes/nodiscard/unsupported_clone.phpt new file mode 100644 index 0000000000000..7fadfea40fa6a --- /dev/null +++ b/Zend/tests/attributes/nodiscard/unsupported_clone.phpt @@ -0,0 +1,16 @@ +--TEST-- +#[\NoDiscard]: Not allowed on '__clone'. +--FILE-- + +--EXPECTF-- +Fatal error: Method Clazz::__clone cannot be #[\NoDiscard] in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/unsupported_constructor.phpt b/Zend/tests/attributes/nodiscard/unsupported_constructor.phpt new file mode 100644 index 0000000000000..a11c6379938d3 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/unsupported_constructor.phpt @@ -0,0 +1,16 @@ +--TEST-- +#[\NoDiscard]: Not allowed on '__construct'. +--FILE-- + +--EXPECTF-- +Fatal error: Method Clazz::__construct cannot be #[\NoDiscard] in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/unsupported_never_function.phpt b/Zend/tests/attributes/nodiscard/unsupported_never_function.phpt new file mode 100644 index 0000000000000..ee7a81acbcde9 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/unsupported_never_function.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\NoDiscard]: Not allowed on never function. +--FILE-- + +--EXPECTF-- +Fatal error: A never returning function does not return a value, but #[\NoDiscard] requires a return value in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/unsupported_property_hook_get.phpt b/Zend/tests/attributes/nodiscard/unsupported_property_hook_get.phpt new file mode 100644 index 0000000000000..ccedf3ede315a --- /dev/null +++ b/Zend/tests/attributes/nodiscard/unsupported_property_hook_get.phpt @@ -0,0 +1,20 @@ +--TEST-- +#[\NoDiscard]: Not allowed on 'get' property hook. +--FILE-- +test; + +?> +--EXPECTF-- +Fatal error: #[\NoDiscard] is not supported for property hooks in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/unsupported_property_hook_set.phpt b/Zend/tests/attributes/nodiscard/unsupported_property_hook_set.phpt new file mode 100644 index 0000000000000..351593f5250bb --- /dev/null +++ b/Zend/tests/attributes/nodiscard/unsupported_property_hook_set.phpt @@ -0,0 +1,20 @@ +--TEST-- +#[\NoDiscard]: Not allowed on 'set' property hook. +--FILE-- +test = $value; + } + } +} + +$cls = new Foo(); +$cls->test = 'foo'; + +?> +--EXPECTF-- +Fatal error: #[\NoDiscard] is not supported for property hooks in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/unsupported_void_function.phpt b/Zend/tests/attributes/nodiscard/unsupported_void_function.phpt new file mode 100644 index 0000000000000..3c45f255473bc --- /dev/null +++ b/Zend/tests/attributes/nodiscard/unsupported_void_function.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\NoDiscard]: Not allowed on void function. +--FILE-- + +--EXPECTF-- +Fatal error: A void function does not return a value, but #[\NoDiscard] requires a return value in %s on line %d diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 5bc4b4a04509f..4c31fba5e506e 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -2698,6 +2698,12 @@ static void zend_check_magic_method_arg_type(uint32_t arg_num, const zend_class_ static void zend_check_magic_method_return_type(const zend_class_entry *ce, const zend_function *fptr, int error_type, int return_type) { + if (return_type == MAY_BE_VOID) { + if (fptr->common.fn_flags & ZEND_ACC_NODISCARD) { + zend_error_noreturn(error_type, "Method %s::%s cannot be #[\\NoDiscard]", ZSTR_VAL(ce->name), ZSTR_VAL(fptr->common.function_name)); + } + } + if (!(fptr->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE)) { /* For backwards compatibility reasons, do not enforce the return type if it is not set. */ return; @@ -2757,6 +2763,10 @@ static void zend_check_magic_method_no_return_type( zend_error_noreturn(error_type, "Method %s::%s() cannot declare a return type", ZSTR_VAL(ce->name), ZSTR_VAL(fptr->common.function_name)); } + + if (fptr->common.fn_flags & ZEND_ACC_NODISCARD) { + zend_error_noreturn(error_type, "Method %s::%s cannot be #[\\NoDiscard]", ZSTR_VAL(ce->name), ZSTR_VAL(fptr->common.function_name)); + } } ZEND_API void zend_check_magic_method_implementation(const zend_class_entry *ce, const zend_function *fptr, zend_string *lcname, int error_type) /* {{{ */ diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index db7e759c8f1a6..b065105d5cf9e 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8413,6 +8413,27 @@ static zend_op_array *zend_compile_func_decl_ex( } } + if (op_array->fn_flags & ZEND_ACC_NODISCARD) { + if (is_hook) { + zend_error_noreturn(E_COMPILE_ERROR, "#[\\NoDiscard] is not supported for property hooks"); + } + + if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { + zend_arg_info *return_info = CG(active_op_array)->arg_info - 1; + if (ZEND_TYPE_CONTAINS_CODE(return_info->type, IS_VOID)) { + zend_error_noreturn(E_COMPILE_ERROR, + "A void %s does not return a value, but #[\\NoDiscard] requires a return value", + CG(active_class_entry) != NULL ? "method" : "function"); + } + + if (ZEND_TYPE_CONTAINS_CODE(return_info->type, IS_NEVER)) { + zend_error_noreturn(E_COMPILE_ERROR, + "A never returning %s does not return a value, but #[\\NoDiscard] requires a return value", + CG(active_class_entry) != NULL ? "method" : "function"); + } + } + } + zend_compile_stmt(stmt_ast); if (is_method) { From 526e6f3ad58175b961c1595b7b8412de7fafc2c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 18 Dec 2024 11:20:25 +0100 Subject: [PATCH 06/11] Make `ZEND_ACC_NODISCARD` functional --- Zend/Optimizer/optimize_func_calls.c | 4 +- Zend/tests/attributes/nodiscard/001.phpt | 86 +++++++++++++++++++ Zend/tests/attributes/nodiscard/002.phpt | 44 ++++++++++ Zend/tests/attributes/nodiscard/003.phpt | 22 +++++ Zend/tests/attributes/nodiscard/005.phpt | 17 ++++ Zend/tests/attributes/nodiscard/006.phpt | 20 +++++ Zend/tests/attributes/nodiscard/007.phpt | 21 +++++ Zend/tests/attributes/nodiscard/008.phpt | 18 ++++ Zend/tests/attributes/nodiscard/009.phpt | 14 +++ .../attributes/nodiscard/error_code_001.phpt | 21 +++++ .../attributes/nodiscard/suppress_assign.phpt | 66 ++++++++++++++ .../attributes/nodiscard/suppress_cast.phpt | 66 ++++++++++++++ .../nodiscard/throwing_error_handler_001.phpt | 35 ++++++++ .../nodiscard/type_validation_001.phpt | 15 ++++ .../nodiscard/type_validation_002.phpt | 20 +++++ .../nodiscard/type_validation_003.phpt | 18 ++++ .../nodiscard/type_validation_004.phpt | 18 ++++ Zend/zend_compile.c | 4 +- Zend/zend_execute.c | 82 ++++++++++++++++++ Zend/zend_execute.h | 1 + Zend/zend_object_handlers.c | 2 +- Zend/zend_vm_def.h | 18 +++- Zend/zend_vm_execute.h | 54 +++++++++--- ext/zend_test/test.c | 7 ++ ext/zend_test/test.stub.php | 5 ++ ext/zend_test/test_arginfo.h | 34 +++++++- 26 files changed, 688 insertions(+), 24 deletions(-) create mode 100644 Zend/tests/attributes/nodiscard/001.phpt create mode 100644 Zend/tests/attributes/nodiscard/002.phpt create mode 100644 Zend/tests/attributes/nodiscard/003.phpt create mode 100644 Zend/tests/attributes/nodiscard/005.phpt create mode 100644 Zend/tests/attributes/nodiscard/006.phpt create mode 100644 Zend/tests/attributes/nodiscard/007.phpt create mode 100644 Zend/tests/attributes/nodiscard/008.phpt create mode 100644 Zend/tests/attributes/nodiscard/009.phpt create mode 100644 Zend/tests/attributes/nodiscard/error_code_001.phpt create mode 100644 Zend/tests/attributes/nodiscard/suppress_assign.phpt create mode 100644 Zend/tests/attributes/nodiscard/suppress_cast.phpt create mode 100644 Zend/tests/attributes/nodiscard/throwing_error_handler_001.phpt create mode 100644 Zend/tests/attributes/nodiscard/type_validation_001.phpt create mode 100644 Zend/tests/attributes/nodiscard/type_validation_002.phpt create mode 100644 Zend/tests/attributes/nodiscard/type_validation_003.phpt create mode 100644 Zend/tests/attributes/nodiscard/type_validation_004.phpt diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c index ce6c43afaedbe..f6d795538b1c5 100644 --- a/Zend/Optimizer/optimize_func_calls.c +++ b/Zend/Optimizer/optimize_func_calls.c @@ -78,8 +78,10 @@ static void zend_delete_call_instructions(zend_op_array *op_array, zend_op *opli static void zend_try_inline_call(zend_op_array *op_array, zend_op *fcall, zend_op *opline, zend_function *func) { + const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; + if (func->type == ZEND_USER_FUNCTION - && !(func->op_array.fn_flags & (ZEND_ACC_ABSTRACT|ZEND_ACC_HAS_TYPE_HINTS|ZEND_ACC_DEPRECATED)) + && !(func->op_array.fn_flags & (ZEND_ACC_ABSTRACT|ZEND_ACC_HAS_TYPE_HINTS|ZEND_ACC_DEPRECATED|no_discard)) /* TODO: function copied from trait may be inconsistent ??? */ && !(func->op_array.fn_flags & (ZEND_ACC_TRAIT_CLONE)) && fcall->extended_value >= func->op_array.required_num_args diff --git a/Zend/tests/attributes/nodiscard/001.phpt b/Zend/tests/attributes/nodiscard/001.phpt new file mode 100644 index 0000000000000..8bec9972aed67 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/001.phpt @@ -0,0 +1,86 @@ +--TEST-- +#[\NoDiscard]: Basic test. +--FILE-- +test(); +$cls->test2(); +Clazz::test3(); + +call_user_func([$cls, "test"]); + +$closure(); + +$closure2(); + +?> +--EXPECTF-- +Warning: The return value of function test() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of function test2() should either be used or intentionally ignored by casting it as (void), this is important in %s on line %d + +Warning: The return value of function test3() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of function test() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of function test() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of method Clazz::test() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of method Clazz::test2() should either be used or intentionally ignored by casting it as (void), this is important in %s on line %d + +Warning: The return value of method Clazz::test3() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of method Clazz::test() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of function {closure:%s:%d}() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of function {closure:%s:%d}() should either be used or intentionally ignored by casting it as (void) in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/002.phpt b/Zend/tests/attributes/nodiscard/002.phpt new file mode 100644 index 0000000000000..618aa2f6286d6 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/002.phpt @@ -0,0 +1,44 @@ +--TEST-- +#[\NoDiscard]: __call(), __callStatic(), and __invoke(). +--FILE-- +test(); +Clazz::test(); +$cls('foo'); + +?> +--EXPECTF-- +Warning: The return value of method Clazz::test() should either be used or intentionally ignored by casting it as (void) in %s on line %d +__call(test) + +Warning: The return value of method Clazz::test() should either be used or intentionally ignored by casting it as (void) in %s on line %d +__callStatic(test) + +Warning: The return value of method Clazz::__invoke() should either be used or intentionally ignored by casting it as (void) in %s on line %d +__invoke(foo) + diff --git a/Zend/tests/attributes/nodiscard/003.phpt b/Zend/tests/attributes/nodiscard/003.phpt new file mode 100644 index 0000000000000..24865f350472c --- /dev/null +++ b/Zend/tests/attributes/nodiscard/003.phpt @@ -0,0 +1,22 @@ +--TEST-- +#[\NoDiscard]: Taken from trait. +--FILE-- +test(); + +?> +--EXPECTF-- +Warning: The return value of method Clazz::test() should either be used or intentionally ignored by casting it as (void) in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/005.phpt b/Zend/tests/attributes/nodiscard/005.phpt new file mode 100644 index 0000000000000..ec8f33e5299d6 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/005.phpt @@ -0,0 +1,17 @@ +--TEST-- +#[\NoDiscard]: Native function and method. +--FILE-- +setTimestamp(0); + +?> +--EXPECTF-- +Warning: The return value of function flock() should either be used or intentionally ignored by casting it as (void), as locking the stream might have failed in %s on line %d + +Warning: The return value of method DateTimeImmutable::setTimestamp() should either be used or intentionally ignored by casting it as (void), as DateTimeImmutable::setTimestamp() does not modify the object itself in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/006.phpt b/Zend/tests/attributes/nodiscard/006.phpt new file mode 100644 index 0000000000000..959b942522204 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/006.phpt @@ -0,0 +1,20 @@ +--TEST-- +#[\NoDiscard]: execute_ex overwritten +--EXTENSIONS-- +zend_test +--INI-- +zend_test.replace_zend_execute_ex=1 +opcache.jit=disable +--FILE-- + +--EXPECTF-- +Warning: The return value of function test() should either be used or intentionally ignored by casting it as (void) in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/007.phpt b/Zend/tests/attributes/nodiscard/007.phpt new file mode 100644 index 0000000000000..84dff0c8e44f6 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/007.phpt @@ -0,0 +1,21 @@ +--TEST-- +#[\NoDiscard]: execute_internal overwritten +--EXTENSIONS-- +zend_test +--INI-- +zend_test.observer.execute_internal=1 +--FILE-- + +--EXPECTF-- + + + +Warning: The return value of function flock() should either be used or intentionally ignored by casting it as (void), as locking the stream might have failed in %s on line %d + + diff --git a/Zend/tests/attributes/nodiscard/008.phpt b/Zend/tests/attributes/nodiscard/008.phpt new file mode 100644 index 0000000000000..6b4635e08d562 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/008.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\NoDiscard]: Combining with #[\Deprecated]. +--FILE-- + +--EXPECTF-- +Deprecated: Function test() is deprecated in %s on line %d + +Warning: The return value of function test() should either be used or intentionally ignored by casting it as (void) in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/009.phpt b/Zend/tests/attributes/nodiscard/009.phpt new file mode 100644 index 0000000000000..ab887db7a14b0 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/009.phpt @@ -0,0 +1,14 @@ +--TEST-- +#[\NoDiscard]: Combining with #[\Deprecated] (Internal). +--EXTENSIONS-- +zend_test +--FILE-- + +--EXPECTF-- +Deprecated: Function zend_test_deprecated_nodiscard() is deprecated, custom message in %s on line %d + +Warning: The return value of function zend_test_deprecated_nodiscard() should either be used or intentionally ignored by casting it as (void), custom message 2 in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/error_code_001.phpt b/Zend/tests/attributes/nodiscard/error_code_001.phpt new file mode 100644 index 0000000000000..2952587db7b87 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/error_code_001.phpt @@ -0,0 +1,21 @@ +--TEST-- +#[\NoDiscard]: Code is E_USER_WARNING. +--FILE-- + +--EXPECT-- +int(512) +int(512) +bool(true) diff --git a/Zend/tests/attributes/nodiscard/suppress_assign.phpt b/Zend/tests/attributes/nodiscard/suppress_assign.phpt new file mode 100644 index 0000000000000..b09f1a39718a8 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/suppress_assign.phpt @@ -0,0 +1,66 @@ +--TEST-- +#[\NoDiscard]: Assigning to variable suppresses. +--FILE-- +test(); +$_ = $cls->test2(); +$_ = call_user_func([$cls, "test"]); +$_ = Clazz::test3(); + +$_ = $closure(); + +$_ = $closure2(); + +?> +DONE +--EXPECT-- +DONE diff --git a/Zend/tests/attributes/nodiscard/suppress_cast.phpt b/Zend/tests/attributes/nodiscard/suppress_cast.phpt new file mode 100644 index 0000000000000..2f639371cb3dd --- /dev/null +++ b/Zend/tests/attributes/nodiscard/suppress_cast.phpt @@ -0,0 +1,66 @@ +--TEST-- +#[\NoDiscard]: Casting to (void) suppresses. +--FILE-- +test(); +(void)$cls->test2(); +(void)call_user_func([$cls, "test"]); +(void)Clazz::test3(); + +(void)$closure(); + +(void)$closure2(); + +?> +DONE +--EXPECT-- +DONE diff --git a/Zend/tests/attributes/nodiscard/throwing_error_handler_001.phpt b/Zend/tests/attributes/nodiscard/throwing_error_handler_001.phpt new file mode 100644 index 0000000000000..51179b26ab194 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/throwing_error_handler_001.phpt @@ -0,0 +1,35 @@ +--TEST-- +#[\NoDiscard]: Throwing error handler. +--FILE-- +getMessage(), PHP_EOL; +} + +#[\NoDiscard] +function test2(): stdClass { + return new stdClass(); +} + +try { + test2(); +} catch (ErrorException $e) { + echo "Caught: ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +Caught: The return value of function test() should either be used or intentionally ignored by casting it as (void) +Caught: The return value of function test2() should either be used or intentionally ignored by casting it as (void) diff --git a/Zend/tests/attributes/nodiscard/type_validation_001.phpt b/Zend/tests/attributes/nodiscard/type_validation_001.phpt new file mode 100644 index 0000000000000..b7a375ada5c07 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/type_validation_001.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\NoDiscard]: Type validation of $message parameter with int. +--FILE-- + +--EXPECTF-- +Warning: The return value of function test() should either be used or intentionally ignored by casting it as (void), 1234 in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/type_validation_002.phpt b/Zend/tests/attributes/nodiscard/type_validation_002.phpt new file mode 100644 index 0000000000000..254fae15ac47a --- /dev/null +++ b/Zend/tests/attributes/nodiscard/type_validation_002.phpt @@ -0,0 +1,20 @@ +--TEST-- +#[\NoDiscard]: Type validation of $message parameter with int and strict types. +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught TypeError: NoDiscard::__construct(): Argument #1 ($message) must be of type ?string, int given in %s:%d +Stack trace: +#0 %s(%d): NoDiscard->__construct(1234) +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/type_validation_003.phpt b/Zend/tests/attributes/nodiscard/type_validation_003.phpt new file mode 100644 index 0000000000000..0b66addaea187 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/type_validation_003.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\NoDiscard]: Type validation of $message parameter with array. +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught TypeError: NoDiscard::__construct(): Argument #1 ($message) must be of type ?string, array given in %s:%d +Stack trace: +#0 %s(%d): NoDiscard->__construct(Array) +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/type_validation_004.phpt b/Zend/tests/attributes/nodiscard/type_validation_004.phpt new file mode 100644 index 0000000000000..44d7c19b54863 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/type_validation_004.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\NoDiscard]: Type validation of $message parameter with native enum case. +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught TypeError: NoDiscard::__construct(): Argument #1 ($message) must be of type ?string, Random\IntervalBoundary given in %s:%d +Stack trace: +#0 %s(%d): NoDiscard->__construct(Random\IntervalBoundary::ClosedOpen) +#1 {main} + thrown in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index b065105d5cf9e..1784a08e8cc67 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3933,7 +3933,7 @@ ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, zend_function *fbc) /* ZEND_ASSERT(!(fbc->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)); if (fbc->type == ZEND_INTERNAL_FUNCTION && !(CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_FUNCTIONS)) { if (init_op->opcode == ZEND_INIT_FCALL && !zend_execute_internal) { - if (!(fbc->common.fn_flags & ZEND_ACC_DEPRECATED)) { + if (!(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { return ZEND_DO_ICALL; } else { return ZEND_DO_FCALL_BY_NAME; @@ -3941,7 +3941,7 @@ ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, zend_function *fbc) /* } } else if (!(CG(compiler_options) & ZEND_COMPILE_IGNORE_USER_FUNCTIONS)){ if (zend_execute_ex == execute_ex) { - if (!(fbc->common.fn_flags & ZEND_ACC_DEPRECATED)) { + if (!(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { return ZEND_DO_UCALL; } else { return ZEND_DO_FCALL_BY_NAME; diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 6b6af2c225f79..132c398476078 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1907,6 +1907,88 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_function(const zend_functi zend_string_release(message_suffix); } +ZEND_COLD static zend_result ZEND_FASTCALL get_nodiscard_suffix_from_attribute(HashTable *attributes, zend_class_entry* scope, zend_string **message_suffix) +{ + *message_suffix = ZSTR_EMPTY_ALLOC(); + + if (!attributes) { + return SUCCESS; + } + + zend_attribute *nodiscard = zend_get_attribute_str(attributes, "nodiscard", sizeof("nodiscard")-1); + + if (!nodiscard) { + return SUCCESS; + } + + if (nodiscard->argc == 0) { + return SUCCESS; + } + + zend_result result = FAILURE; + + zend_string *message = ZSTR_EMPTY_ALLOC(); + + zval obj; + ZVAL_UNDEF(&obj); + zval *z; + + /* Construct the NoDiscard object to correctly handle parameter processing. */ + if (FAILURE == zend_get_attribute_object(&obj, zend_ce_nodiscard, nodiscard, scope, NULL)) { + goto out; + } + + /* Extract the $message property. */ + z = zend_read_property_ex(zend_ce_nodiscard, Z_OBJ_P(&obj), ZSTR_KNOWN(ZEND_STR_MESSAGE), false, NULL); + ZEND_ASSERT(z != &EG(uninitialized_zval)); + if (Z_TYPE_P(z) == IS_STRING) { + message = zend_string_copy(Z_STR_P(z)); + } + + /* Construct the suffix. */ + *message_suffix = zend_strpprintf_unchecked( + 0, + "%s%S", + ZSTR_LEN(message) > 0 ? ", " : "", + message + ); + + result = SUCCESS; + + out: + + zend_string_release(message); + zval_ptr_dtor(&obj); + + return result; +} + +ZEND_API ZEND_COLD void ZEND_FASTCALL zend_nodiscard_function(const zend_function *fbc) +{ + zend_string *message_suffix = ZSTR_EMPTY_ALLOC(); + + if (get_nodiscard_suffix_from_attribute(fbc->common.attributes, fbc->common.scope, &message_suffix) == FAILURE) { + return; + } + + int code = fbc->type == ZEND_INTERNAL_FUNCTION ? E_WARNING : E_USER_WARNING; + + if (fbc->common.scope) { + zend_error_unchecked(code, "The return value of method %s::%s() should either be used or intentionally ignored by casting it as (void)%S", + ZSTR_VAL(fbc->common.scope->name), + ZSTR_VAL(fbc->common.function_name), + message_suffix + ); + } else { + zend_error_unchecked(code, "The return value of function %s() should either be used or intentionally ignored by casting it as (void)%S", + ZSTR_VAL(fbc->common.function_name), + message_suffix + ); + } + + zend_string_release(message_suffix); +} + ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_class_constant(const zend_class_constant *c, const zend_string *constant_name) { zend_string *message_suffix = ZSTR_EMPTY_ALLOC(); diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index 3b8ed89ec4f38..e96a217a2904f 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -62,6 +62,7 @@ extern ZEND_API const zend_internal_function zend_pass_function; ZEND_API ZEND_COLD void ZEND_FASTCALL zend_missing_arg_error(zend_execute_data *execute_data); ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_function(const zend_function *fbc); +ZEND_API ZEND_COLD void ZEND_FASTCALL zend_nodiscard_function(const zend_function *fbc); ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_class_constant(const zend_class_constant *c, const zend_string *constant_name); ZEND_API ZEND_COLD void ZEND_FASTCALL zend_false_to_array_deprecated(void); ZEND_COLD void ZEND_FASTCALL zend_param_must_be_ref(const zend_function *func, uint32_t arg_num); diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 3b3ecfc590d2a..ca6e523bc3ab9 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1636,7 +1636,7 @@ ZEND_API zend_function *zend_get_call_trampoline_func(const zend_class_entry *ce func->fn_flags = ZEND_ACC_CALL_VIA_TRAMPOLINE | ZEND_ACC_PUBLIC | ZEND_ACC_VARIADIC - | (fbc->common.fn_flags & (ZEND_ACC_RETURN_REFERENCE|ZEND_ACC_ABSTRACT|ZEND_ACC_DEPRECATED)); + | (fbc->common.fn_flags & (ZEND_ACC_RETURN_REFERENCE|ZEND_ACC_ABSTRACT|ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD)); /* Attributes outlive the trampoline because they are created by the compiler. */ func->attributes = fbc->common.attributes; if (is_static) { diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 6c87b81bc3bd7..0fd4c6f09f84e 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4156,8 +4156,13 @@ ZEND_VM_HOT_HANDLER(131, ZEND_DO_FCALL_BY_NAME, ANY, ANY, SPEC(RETVAL,OBSERVER)) SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { - zend_deprecated_function(fbc); + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + zend_deprecated_function(fbc); + } + if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + zend_nodiscard_function(fbc); + } if (UNEXPECTED(EG(exception) != NULL)) { UNDEF_RESULT(); if (!RETURN_VALUE_USED(opline)) { @@ -4260,8 +4265,13 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER)) SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { - zend_deprecated_function(fbc); + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + zend_deprecated_function(fbc); + } + if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + zend_nodiscard_function(fbc); + } if (UNEXPECTED(EG(exception) != NULL)) { if (UNEXPECTED(ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE)) { OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 06b4ad6b1494a..fbec651786205 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -1552,8 +1552,13 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_S SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { - zend_deprecated_function(fbc); + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + zend_deprecated_function(fbc); + } + if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !0) { + zend_nodiscard_function(fbc); + } if (UNEXPECTED(EG(exception) != NULL)) { UNDEF_RESULT(); if (!0) { @@ -1654,8 +1659,13 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_S SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { - zend_deprecated_function(fbc); + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + zend_deprecated_function(fbc); + } + if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !1) { + zend_nodiscard_function(fbc); + } if (UNEXPECTED(EG(exception) != NULL)) { UNDEF_RESULT(); if (!1) { @@ -1756,8 +1766,13 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_ SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { - zend_deprecated_function(fbc); + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + zend_deprecated_function(fbc); + } + if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + zend_nodiscard_function(fbc); + } if (UNEXPECTED(EG(exception) != NULL)) { UNDEF_RESULT(); if (!RETURN_VALUE_USED(opline)) { @@ -1860,8 +1875,13 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_RETV SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { - zend_deprecated_function(fbc); + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + zend_deprecated_function(fbc); + } + if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !0) { + zend_nodiscard_function(fbc); + } if (UNEXPECTED(EG(exception) != NULL)) { if (UNEXPECTED(ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE)) { OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); @@ -1978,8 +1998,13 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_RETV SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { - zend_deprecated_function(fbc); + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + zend_deprecated_function(fbc); + } + if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !1) { + zend_nodiscard_function(fbc); + } if (UNEXPECTED(EG(exception) != NULL)) { if (UNEXPECTED(ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE)) { OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); @@ -2096,8 +2121,13 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_OBS SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED((fbc->common.fn_flags & ZEND_ACC_DEPRECATED) != 0)) { - zend_deprecated_function(fbc); + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + zend_deprecated_function(fbc); + } + if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + zend_nodiscard_function(fbc); + } if (UNEXPECTED(EG(exception) != NULL)) { if (UNEXPECTED(ZEND_CALL_INFO(call) & ZEND_CALL_CLOSURE)) { OBJ_RELEASE(ZEND_CLOSURE_OBJECT(call->func)); diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index e3f87ee1e1636..b2124b07060a9 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -123,6 +123,13 @@ static ZEND_FUNCTION(zend_test_deprecated_attr) ZEND_PARSE_PARAMETERS_NONE(); } +static ZEND_FUNCTION(zend_test_deprecated_nodiscard) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_LONG(1); +} + /* Create a string without terminating null byte. Must be terminated with * zend_terminate_string() before destruction, otherwise a warning is issued * in debug builds. */ diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 59cb9661e4e43..88007ae01fe56 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -218,6 +218,11 @@ function zend_test_deprecated(mixed $arg = null): void {} #[\Deprecated(message: "custom message")] function zend_test_deprecated_attr(): void {} + + #[\Deprecated(message: "custom message")] + #[\NoDiscard(message: "custom message 2")] + function zend_test_deprecated_nodiscard(): int {} + /** @alias zend_test_void_return */ function zend_test_aliased(): void {} diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index c558b58f65169..fc9a4e654bc7d 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 3082e62e96d5f4383c98638513463c676a7c3a69 */ + * Stub hash: 9e70ab275967137c764d0983ea09a95f7594e799 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -22,6 +22,9 @@ ZEND_END_ARG_INFO() #define arginfo_zend_test_deprecated_attr arginfo_zend_test_void_return +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_deprecated_nodiscard, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + #define arginfo_zend_test_aliased arginfo_zend_test_void_return #define arginfo_zend_test_deprecated_aliased arginfo_zend_test_void_return @@ -126,8 +129,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_is_string_marked_as_va ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_get_map_ptr_last, 0, 0, IS_LONG, 0) -ZEND_END_ARG_INFO() +#define arginfo_zend_get_map_ptr_last arginfo_zend_test_deprecated_nodiscard ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_crash, 0, 0, IS_VOID, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, message, IS_STRING, 1, "null") @@ -185,7 +187,7 @@ ZEND_END_ARG_INFO() #define arginfo_ZendTestNS2_ZendSubNS_namespaced_deprecated_aliased_func arginfo_zend_test_void_return -#define arginfo_class__ZendTestClass_is_object arginfo_zend_get_map_ptr_last +#define arginfo_class__ZendTestClass_is_object arginfo_zend_test_deprecated_nodiscard #define arginfo_class__ZendTestClass___toString arginfo_zend_get_current_func_name @@ -260,6 +262,7 @@ static ZEND_FUNCTION(zend_test_void_return); static ZEND_FUNCTION(zend_test_compile_string); static ZEND_FUNCTION(zend_test_deprecated); static ZEND_FUNCTION(zend_test_deprecated_attr); +static ZEND_FUNCTION(zend_test_deprecated_nodiscard); static ZEND_FUNCTION(zend_create_unterminated_string); static ZEND_FUNCTION(zend_terminate_string); static ZEND_FUNCTION(zend_leak_variable); @@ -355,6 +358,11 @@ static const zend_function_entry ext_functions[] = { #else ZEND_RAW_FENTRY("zend_test_deprecated_attr", zif_zend_test_deprecated_attr, arginfo_zend_test_deprecated_attr, ZEND_ACC_DEPRECATED) #endif +#if (PHP_VERSION_ID >= 80400) + ZEND_RAW_FENTRY("zend_test_deprecated_nodiscard", zif_zend_test_deprecated_nodiscard, arginfo_zend_test_deprecated_nodiscard, ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD, NULL, NULL) +#else + ZEND_RAW_FENTRY("zend_test_deprecated_nodiscard", zif_zend_test_deprecated_nodiscard, arginfo_zend_test_deprecated_nodiscard, ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD) +#endif #if (PHP_VERSION_ID >= 80400) ZEND_RAW_FENTRY("zend_test_aliased", zif_zend_test_void_return, arginfo_zend_test_aliased, 0, NULL, NULL) #else @@ -560,6 +568,24 @@ static void register_test_symbols(int module_number) ZVAL_COPY_VALUE(&attribute_Deprecated_func_zend_test_deprecated_attr_0->args[0].value, &attribute_Deprecated_func_zend_test_deprecated_attr_0_arg0); attribute_Deprecated_func_zend_test_deprecated_attr_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + zend_string *attribute_name_Deprecated_func_zend_test_deprecated_nodiscard_0 = zend_string_init_interned("Deprecated", sizeof("Deprecated") - 1, 1); + zend_attribute *attribute_Deprecated_func_zend_test_deprecated_nodiscard_0 = zend_add_function_attribute(zend_hash_str_find_ptr(CG(function_table), "zend_test_deprecated_nodiscard", sizeof("zend_test_deprecated_nodiscard") - 1), attribute_name_Deprecated_func_zend_test_deprecated_nodiscard_0, 1); + zend_string_release(attribute_name_Deprecated_func_zend_test_deprecated_nodiscard_0); + zval attribute_Deprecated_func_zend_test_deprecated_nodiscard_0_arg0; + zend_string *attribute_Deprecated_func_zend_test_deprecated_nodiscard_0_arg0_str = zend_string_init("custom message", strlen("custom message"), 1); + ZVAL_STR(&attribute_Deprecated_func_zend_test_deprecated_nodiscard_0_arg0, attribute_Deprecated_func_zend_test_deprecated_nodiscard_0_arg0_str); + ZVAL_COPY_VALUE(&attribute_Deprecated_func_zend_test_deprecated_nodiscard_0->args[0].value, &attribute_Deprecated_func_zend_test_deprecated_nodiscard_0_arg0); + attribute_Deprecated_func_zend_test_deprecated_nodiscard_0->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + + zend_string *attribute_name_NoDiscard_func_zend_test_deprecated_nodiscard_1 = zend_string_init_interned("NoDiscard", sizeof("NoDiscard") - 1, 1); + zend_attribute *attribute_NoDiscard_func_zend_test_deprecated_nodiscard_1 = zend_add_function_attribute(zend_hash_str_find_ptr(CG(function_table), "zend_test_deprecated_nodiscard", sizeof("zend_test_deprecated_nodiscard") - 1), attribute_name_NoDiscard_func_zend_test_deprecated_nodiscard_1, 1); + zend_string_release(attribute_name_NoDiscard_func_zend_test_deprecated_nodiscard_1); + zval attribute_NoDiscard_func_zend_test_deprecated_nodiscard_1_arg0; + zend_string *attribute_NoDiscard_func_zend_test_deprecated_nodiscard_1_arg0_str = zend_string_init("custom message 2", strlen("custom message 2"), 1); + ZVAL_STR(&attribute_NoDiscard_func_zend_test_deprecated_nodiscard_1_arg0, attribute_NoDiscard_func_zend_test_deprecated_nodiscard_1_arg0_str); + ZVAL_COPY_VALUE(&attribute_NoDiscard_func_zend_test_deprecated_nodiscard_1->args[0].value, &attribute_NoDiscard_func_zend_test_deprecated_nodiscard_1_arg0); + attribute_NoDiscard_func_zend_test_deprecated_nodiscard_1->args[0].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); + zend_string *attribute_name_ZendTestParameterAttribute_func_zend_test_parameter_with_attribute_arg0_0 = zend_string_init_interned("ZendTestParameterAttribute", sizeof("ZendTestParameterAttribute") - 1, 1); zend_attribute *attribute_ZendTestParameterAttribute_func_zend_test_parameter_with_attribute_arg0_0 = zend_add_parameter_attribute(zend_hash_str_find_ptr(CG(function_table), "zend_test_parameter_with_attribute", sizeof("zend_test_parameter_with_attribute") - 1), 0, attribute_name_ZendTestParameterAttribute_func_zend_test_parameter_with_attribute_arg0_0, 1); zend_string_release(attribute_name_ZendTestParameterAttribute_func_zend_test_parameter_with_attribute_arg0_0); From 1bd1d6c0cfb96467e121d839576a39ed4041ab15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 24 Jan 2025 23:10:25 +0100 Subject: [PATCH 07/11] Leverage RETVAL opcode specialization --- Zend/zend_vm_def.h | 12 ++++++++---- Zend/zend_vm_execute.h | 36 ++++++++++++++++++++++++------------ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 0fd4c6f09f84e..563928c9dbf59 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -4156,11 +4156,13 @@ ZEND_VM_HOT_HANDLER(131, ZEND_DO_FCALL_BY_NAME, ANY, ANY, SPEC(RETVAL,OBSERVER)) SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; + + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { zend_deprecated_function(fbc); } - if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + if ((fbc->common.fn_flags & no_discard) && EG(exception) == NULL) { zend_nodiscard_function(fbc); } if (UNEXPECTED(EG(exception) != NULL)) { @@ -4265,11 +4267,13 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER)) SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; + + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { zend_deprecated_function(fbc); } - if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + if ((fbc->common.fn_flags & no_discard) && EG(exception) == NULL) { zend_nodiscard_function(fbc); } if (UNEXPECTED(EG(exception) != NULL)) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index fbec651786205..97466dab76a94 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -1552,11 +1552,13 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_S SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + const uint32_t no_discard = 0 ? 0 : ZEND_ACC_NODISCARD; + + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { zend_deprecated_function(fbc); } - if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !0) { + if ((fbc->common.fn_flags & no_discard) && EG(exception) == NULL) { zend_nodiscard_function(fbc); } if (UNEXPECTED(EG(exception) != NULL)) { @@ -1659,11 +1661,13 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_S SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + const uint32_t no_discard = 1 ? 0 : ZEND_ACC_NODISCARD; + + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { zend_deprecated_function(fbc); } - if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !1) { + if ((fbc->common.fn_flags & no_discard) && EG(exception) == NULL) { zend_nodiscard_function(fbc); } if (UNEXPECTED(EG(exception) != NULL)) { @@ -1766,11 +1770,13 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_BY_NAME_ SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; + + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { zend_deprecated_function(fbc); } - if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + if ((fbc->common.fn_flags & no_discard) && EG(exception) == NULL) { zend_nodiscard_function(fbc); } if (UNEXPECTED(EG(exception) != NULL)) { @@ -1875,11 +1881,13 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_RETV SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + const uint32_t no_discard = 0 ? 0 : ZEND_ACC_NODISCARD; + + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { zend_deprecated_function(fbc); } - if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !0) { + if ((fbc->common.fn_flags & no_discard) && EG(exception) == NULL) { zend_nodiscard_function(fbc); } if (UNEXPECTED(EG(exception) != NULL)) { @@ -1998,11 +2006,13 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_RETV SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + const uint32_t no_discard = 1 ? 0 : ZEND_ACC_NODISCARD; + + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { zend_deprecated_function(fbc); } - if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !1) { + if ((fbc->common.fn_flags & no_discard) && EG(exception) == NULL) { zend_nodiscard_function(fbc); } if (UNEXPECTED(EG(exception) != NULL)) { @@ -2121,11 +2131,13 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_FCALL_SPEC_OBS SAVE_OPLINE(); EX(call) = call->prev_execute_data; - if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; + + if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { zend_deprecated_function(fbc); } - if (EG(exception) == NULL && (fbc->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + if ((fbc->common.fn_flags & no_discard) && EG(exception) == NULL) { zend_nodiscard_function(fbc); } if (UNEXPECTED(EG(exception) != NULL)) { From 368e73d3b7411c9ec05e96339b673bee75bb4b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 28 Jan 2025 15:21:17 +0100 Subject: [PATCH 08/11] JIT support --- ext/opcache/jit/zend_jit_internal.h | 2 ++ ext/opcache/jit/zend_jit_ir.c | 42 +++++++++++++++-------- ext/opcache/jit/zend_jit_vm_helpers.c | 48 +++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/ext/opcache/jit/zend_jit_internal.h b/ext/opcache/jit/zend_jit_internal.h index e769c6caefb98..b1511c5c1c4df 100644 --- a/ext/opcache/jit/zend_jit_internal.h +++ b/ext/opcache/jit/zend_jit_internal.h @@ -233,6 +233,8 @@ ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_jit_loop_counter_helper(ZEND_OPCODE_H void ZEND_FASTCALL zend_jit_copy_extra_args_helper(EXECUTE_DATA_D); void ZEND_FASTCALL zend_jit_copy_extra_args_helper_no_skip_recv(EXECUTE_DATA_D); bool ZEND_FASTCALL zend_jit_deprecated_helper(OPLINE_D); +bool ZEND_FASTCALL zend_jit_nodiscard_helper(OPLINE_D); +bool ZEND_FASTCALL zend_jit_deprecated_nodiscard_helper(OPLINE_D); void ZEND_FASTCALL zend_jit_undefined_long_key(EXECUTE_DATA_D); void ZEND_FASTCALL zend_jit_undefined_long_key_ex(zend_long key EXECUTE_DATA_DC); void ZEND_FASTCALL zend_jit_undefined_string_key(EXECUTE_DATA_D); diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 69c80ff63304e..4d006c19d34e9 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -10153,7 +10153,7 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen ir_GUARD_NOT( ir_AND_U32( ir_LOAD_U32(ir_ADD_OFFSET(func_ref, offsetof(zend_op_array, fn_flags))), - ir_CONST_U32(ZEND_ACC_DEPRECATED)), + ir_CONST_U32(ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD)), ir_CONST_ADDR(exit_addr)); } } @@ -10179,12 +10179,27 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen if (opline->opcode == ZEND_DO_FCALL || opline->opcode == ZEND_DO_FCALL_BY_NAME) { if (!func) { if (!trace) { - ir_ref if_deprecated, ret; + ir_ref if_deprecated_nodiscard, ret; - if_deprecated = ir_IF(ir_AND_U32( + uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; + + if_deprecated_nodiscard = ir_IF(ir_AND_U32( ir_LOAD_U32(ir_ADD_OFFSET(func_ref, offsetof(zend_op_array, fn_flags))), - ir_CONST_U32(ZEND_ACC_DEPRECATED))); - ir_IF_TRUE_cold(if_deprecated); + ir_CONST_U32(ZEND_ACC_DEPRECATED|no_discard))); + ir_IF_TRUE_cold(if_deprecated_nodiscard); + + ir_ref helper = ir_CONST_FC_FUNC(no_discard ? zend_jit_deprecated_nodiscard_helper : zend_jit_deprecated_helper); + if (GCC_GLOBAL_REGS) { + ret = ir_CALL(IR_BOOL, helper); + } else { + ret = ir_CALL_1(IR_BOOL, helper, rx); + } + ir_GUARD(ret, jit_STUB_ADDR(jit, jit_stub_exception_handler)); + ir_MERGE_WITH_EMPTY_FALSE(if_deprecated_nodiscard); + } + } else { + if (func->common.fn_flags & ZEND_ACC_DEPRECATED) { + ir_ref ret; if (GCC_GLOBAL_REGS) { ret = ir_CALL(IR_BOOL, ir_CONST_FC_FUNC(zend_jit_deprecated_helper)); @@ -10192,17 +10207,18 @@ static int zend_jit_do_fcall(zend_jit_ctx *jit, const zend_op *opline, const zen ret = ir_CALL_1(IR_BOOL, ir_CONST_FC_FUNC(zend_jit_deprecated_helper), rx); } ir_GUARD(ret, jit_STUB_ADDR(jit, jit_stub_exception_handler)); - ir_MERGE_WITH_EMPTY_FALSE(if_deprecated); } - } else if (func->common.fn_flags & ZEND_ACC_DEPRECATED) { - ir_ref ret; - if (GCC_GLOBAL_REGS) { - ret = ir_CALL(IR_BOOL, ir_CONST_FC_FUNC(zend_jit_deprecated_helper)); - } else { - ret = ir_CALL_1(IR_BOOL, ir_CONST_FC_FUNC(zend_jit_deprecated_helper), rx); + if ((func->common.fn_flags & ZEND_ACC_NODISCARD) && !RETURN_VALUE_USED(opline)) { + ir_ref ret; + + if (GCC_GLOBAL_REGS) { + ret = ir_CALL(IR_BOOL, ir_CONST_FC_FUNC(zend_jit_nodiscard_helper)); + } else { + ret = ir_CALL_1(IR_BOOL, ir_CONST_FC_FUNC(zend_jit_nodiscard_helper), rx); + } + ir_GUARD(ret, jit_STUB_ADDR(jit, jit_stub_exception_handler)); } - ir_GUARD(ret, jit_STUB_ADDR(jit, jit_stub_exception_handler)); } } diff --git a/ext/opcache/jit/zend_jit_vm_helpers.c b/ext/opcache/jit/zend_jit_vm_helpers.c index b7fb661615d1c..c98376d5a1f69 100644 --- a/ext/opcache/jit/zend_jit_vm_helpers.c +++ b/ext/opcache/jit/zend_jit_vm_helpers.c @@ -204,6 +204,54 @@ bool ZEND_FASTCALL zend_jit_deprecated_helper(OPLINE_D) return 1; } +bool ZEND_FASTCALL zend_jit_nodiscard_helper(OPLINE_D) +{ + zend_execute_data *call = (zend_execute_data *) opline; + zend_function *fbc = call->func; + + zend_nodiscard_function(fbc); + + if (EG(exception)) { +#ifndef HAVE_GCC_GLOBAL_REGS + zend_execute_data *execute_data = EG(current_execute_data); +#endif + const zend_op *opline = EG(opline_before_exception); + if (opline && RETURN_VALUE_USED(opline)) { + ZVAL_UNDEF(EX_VAR(opline->result.var)); + } + + zend_vm_stack_free_args(call); + + if (UNEXPECTED(ZEND_CALL_INFO(call) & ZEND_CALL_RELEASE_THIS)) { + OBJ_RELEASE(Z_OBJ(call->This)); + } + + zend_vm_stack_free_call_frame(call); + return 0; + } + return 1; +} + +bool ZEND_FASTCALL zend_jit_deprecated_nodiscard_helper(OPLINE_D) +{ + zend_execute_data *call = (zend_execute_data *) opline; + zend_function *fbc = call->func; + + if (fbc->common.fn_flags & ZEND_ACC_DEPRECATED) { + if (zend_jit_deprecated_helper(OPLINE_C) == 0) { + return 0; + } + } + + if (fbc->common.fn_flags & ZEND_ACC_NODISCARD) { + if (zend_jit_nodiscard_helper(OPLINE_C) == 0) { + return 0; + } + } + + return 1; +} + void ZEND_FASTCALL zend_jit_undefined_long_key(EXECUTE_DATA_D) { const zend_op *opline = EX(opline); From 609fcf56325b0cb14e73a565caced9adb8bffd63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 26 Mar 2025 17:52:22 +0100 Subject: [PATCH 09/11] Keep optimized opcodes if the result is known-used --- Zend/Optimizer/optimize_func_calls.c | 16 +++-- Zend/tests/attributes/nodiscard/010.phpt | 76 ++++++++++++++++++++++++ Zend/zend_compile.c | 21 +++++-- Zend/zend_compile.h | 2 +- 4 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 Zend/tests/attributes/nodiscard/010.phpt diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c index f6d795538b1c5..8b29f47c94976 100644 --- a/Zend/Optimizer/optimize_func_calls.c +++ b/Zend/Optimizer/optimize_func_calls.c @@ -204,18 +204,12 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx) fcall->op1.num = zend_vm_calc_used_stack(fcall->extended_value, call_stack[call].func); literal_dtor(&ZEND_OP2_LITERAL(fcall)); fcall->op2.constant = fcall->op2.constant + 1; - if (opline->opcode != ZEND_CALLABLE_CONVERT) { - opline->opcode = zend_get_call_op(fcall, call_stack[call].func); - } } else if (fcall->opcode == ZEND_INIT_NS_FCALL_BY_NAME) { fcall->opcode = ZEND_INIT_FCALL; fcall->op1.num = zend_vm_calc_used_stack(fcall->extended_value, call_stack[call].func); literal_dtor(&op_array->literals[fcall->op2.constant]); literal_dtor(&op_array->literals[fcall->op2.constant + 2]); fcall->op2.constant = fcall->op2.constant + 1; - if (opline->opcode != ZEND_CALLABLE_CONVERT) { - opline->opcode = zend_get_call_op(fcall, call_stack[call].func); - } } else if (fcall->opcode == ZEND_INIT_STATIC_METHOD_CALL || fcall->opcode == ZEND_INIT_METHOD_CALL || fcall->opcode == ZEND_INIT_PARENT_PROPERTY_HOOK_CALL @@ -225,6 +219,16 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx) ZEND_UNREACHABLE(); } + /* If the INIT opcode changed the DO opcode can also change to + * a more optimized one. + * + * At this point we also know whether or not the result of + * the DO opcode is used, allowing to optimize calls to + * ZEND_ACC_NODISCARD functions. */ + if (opline->opcode != ZEND_CALLABLE_CONVERT) { + opline->opcode = zend_get_call_op(fcall, call_stack[call].func, !RESULT_UNUSED(opline)); + } + if ((ZEND_OPTIMIZER_PASS_16 & ctx->optimization_level) && call_stack[call].try_inline && opline->opcode != ZEND_CALLABLE_CONVERT) { diff --git a/Zend/tests/attributes/nodiscard/010.phpt b/Zend/tests/attributes/nodiscard/010.phpt new file mode 100644 index 0000000000000..2de81d76aa6ae --- /dev/null +++ b/Zend/tests/attributes/nodiscard/010.phpt @@ -0,0 +1,76 @@ +--TEST-- +#[\NoDiscard]: Functions with known-used result use DO_[IU]CALL opcodes +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.optimization_level=-1 +opcache.opt_debug_level=0x20000 +--EXTENSIONS-- +opcache +--FILE-- + +--EXPECTF-- +$_main: + ; (lines=29, args=0, vars=3, tmps=1) + ; (after optimizer) + ; %s +0000 INIT_FCALL 0 %d string("tmpfile") +0001 V3 = DO_ICALL +0002 ASSIGN CV0($f) V3 +0003 INIT_FCALL 2 %d string("flock") +0004 SEND_VAR CV0($f) 1 +0005 SEND_VAL int(5) 2 +0006 DO_FCALL_BY_NAME +0007 INIT_FCALL 2 %d string("flock") +0008 SEND_VAR CV0($f) 1 +0009 SEND_VAL int(5) 2 +0010 V3 = DO_ICALL +0011 FREE V3 +0012 INIT_FCALL 2 %d string("flock") +0013 SEND_VAR CV0($f) 1 +0014 SEND_VAL int(5) 2 +0015 V3 = DO_ICALL +0016 ASSIGN CV1($success) V3 +0017 INIT_FCALL 1 %d string("fclose") +0018 SEND_VAR CV0($f) 1 +0019 DO_ICALL +0020 INIT_FCALL 0 %d string("test") +0021 DO_FCALL_BY_NAME +0022 INIT_FCALL 0 %d string("test") +0023 V3 = DO_UCALL +0024 FREE V3 +0025 INIT_FCALL 0 %d string("test") +0026 V3 = DO_UCALL +0027 ASSIGN CV2($obj) V3 +0028 RETURN int(1) + +test: + ; (lines=3, args=0, vars=0, tmps=1) + ; (after optimizer) + ; %s +0000 V0 = NEW 0 string("stdClass") +0001 DO_FCALL +0002 RETURN V0 +LIVE RANGES: + 0: 0001 - 0002 (new) + +Warning: The return value of function flock() should either be used or intentionally ignored by casting it as (void), as locking the stream might have failed in %s on line %d + +Warning: The return value of function test() should either be used or intentionally ignored by casting it as (void) in %s on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 1784a08e8cc67..c1988fdaa1535 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -3927,13 +3927,15 @@ static uint32_t zend_compile_args( } /* }}} */ -ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, zend_function *fbc) /* {{{ */ +ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, zend_function *fbc, bool result_used) /* {{{ */ { - if (fbc) { + uint32_t no_discard = result_used ? 0 : ZEND_ACC_NODISCARD; + + if (fbc && init_op->opcode != ZEND_NEW) { ZEND_ASSERT(!(fbc->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)); if (fbc->type == ZEND_INTERNAL_FUNCTION && !(CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_FUNCTIONS)) { if (init_op->opcode == ZEND_INIT_FCALL && !zend_execute_internal) { - if (!(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (!(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { return ZEND_DO_ICALL; } else { return ZEND_DO_FCALL_BY_NAME; @@ -3941,7 +3943,7 @@ ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, zend_function *fbc) /* } } else if (!(CG(compiler_options) & ZEND_COMPILE_IGNORE_USER_FUNCTIONS)){ if (zend_execute_ex == execute_ex) { - if (!(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|ZEND_ACC_NODISCARD))) { + if (!(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) { return ZEND_DO_UCALL; } else { return ZEND_DO_FCALL_BY_NAME; @@ -3991,7 +3993,16 @@ static bool zend_compile_call_common(znode *result, zend_ast *args_ast, zend_fun opline->op1.num = zend_vm_calc_used_stack(arg_count, fbc); } - opline = zend_emit_op(result, zend_get_call_op(opline, fbc), NULL, NULL); + uint8_t call_op = zend_get_call_op( + opline, + fbc, + /* result_used: At this point we do not yet reliably + * know if the result is used. Deoptimize #[\NoDiscard] + * calls to be sure. The optimizer will fix this up. + */ + false + ); + opline = zend_emit_op(result, call_op, NULL, NULL); if (may_have_extra_named_args) { opline->extended_value = ZEND_FCALL_MAY_HAVE_EXTRA_NAMED_PARAMS; } diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index cae695fe7d7ec..db087bdd60035 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -984,7 +984,7 @@ ZEND_API bool zend_is_compiling(void); ZEND_API char *zend_make_compiled_string_description(const char *name); ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_handlers); uint32_t zend_get_class_fetch_type(const zend_string *name); -ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, zend_function *fbc); +ZEND_API uint8_t zend_get_call_op(const zend_op *init_op, zend_function *fbc, bool result_used); ZEND_API bool zend_is_smart_branch(const zend_op *opline); typedef bool (*zend_auto_global_callback)(zend_string *name); From 08222df3083301946e01fa205a1faa5164b2682b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 2 Apr 2025 08:49:54 +0200 Subject: [PATCH 10/11] Remove useless refcounting of `message` in `get_nodiscard_suffix_from_attribute()` --- Zend/zend_execute.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 132c398476078..e7f7d19118d3b 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1942,7 +1942,7 @@ ZEND_COLD static zend_result ZEND_FASTCALL get_nodiscard_suffix_from_attribute(H z = zend_read_property_ex(zend_ce_nodiscard, Z_OBJ_P(&obj), ZSTR_KNOWN(ZEND_STR_MESSAGE), false, NULL); ZEND_ASSERT(z != &EG(uninitialized_zval)); if (Z_TYPE_P(z) == IS_STRING) { - message = zend_string_copy(Z_STR_P(z)); + message = Z_STR_P(z); } /* Construct the suffix. */ @@ -1957,7 +1957,6 @@ ZEND_COLD static zend_result ZEND_FASTCALL get_nodiscard_suffix_from_attribute(H out: - zend_string_release(message); zval_ptr_dtor(&obj); return result; From 97f3f17c47e83e34f7bbbafa8bfc2b5460c098a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 2 Apr 2025 08:55:36 +0200 Subject: [PATCH 11/11] NEWS / UPGRADING --- NEWS | 4 +++- UPGRADING | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index 1a635fc47cef6..65cebd217e130 100644 --- a/NEWS +++ b/NEWS @@ -38,8 +38,10 @@ PHP NEWS . Fixed bug GH-18033 (NULL-ptr dereference when using register_tick_function in destructor). (nielsdos) . Fixed bug GH-18026 (Improve "expecting token" error for ampersand). (ilutov) + . Added the #[\NoDiscard] attribute to indicate that a function's return + value is important and should be consumed. (timwolla, Volker Dusch) . Added the (void) cast to indicate that not using a value is intentional. - (timwolla) + (timwolla, Volker Dusch) . Added get_error_handler(), get_exception_handler() functions. (Arnaud) . Fixed bug GH-15753 and GH-16198 (Bind traits before parent class). (ilutov) diff --git a/UPGRADING b/UPGRADING index 791660451359a..c8d45b54544d6 100644 --- a/UPGRADING +++ b/UPGRADING @@ -114,8 +114,13 @@ PHP 8.5 UPGRADE NOTES . Fatal Errors (such as an exceeded maximum execution time) now include a backtrace. RFC: https://wiki.php.net/rfc/error_backtraces_v2 - . Added the (void) to indicate that not using a value is intentional. The - (void) cast does nothing by itself. + . Added the #[\NoDiscard] attribute to indicate that a function's return + value is important and should be consumed. + RFC: https://wiki.php.net/rfc/marking_return_value_as_important + . Added the (void) cast to indicate that not using a value is intentional. + The (void) cast has no effect on the program's execution by itself, but + it can be used to suppress warnings emitted by #[\NoDiscard] and possibly + also diagnostics emitted by external IDEs or static analysis tools. RFC: https://wiki.php.net/rfc/marking_return_value_as_important - Curl: