From bd473756236bda70a418fbac2c04797d0ef80089 Mon Sep 17 00:00:00 2001 From: Artem Ukrainskiy Date: Thu, 8 Oct 2026 18:38:01 +0300 Subject: [PATCH] Zend: Read string arguments of #[\Deprecated] without constructing it Emitting the deprecation of a function constructed a \Deprecated object on every call to read its message and since arguments: an object, a constructor call with named arguments, two property writes, two reads and the destruction, about 2,900 of the 5,000 instructions a deprecated call costs, paid whether or not E_DEPRECATED is reported. The object was constructed for the parameter handling of the constructor, named arguments, defaults, type errors, constant expressions, and nearly every attribute has only string literals, which need none of it. Read message and since straight from the arguments when they are strings or null, positional or named, each at most once; everything else, a constant expression, another type, a duplicate or unknown name, goes through the constructor as before, and so does a call with an exception pending, where the constructor is not run and nothing is emitted. The same for #[\NoDiscard]. The message is byte for byte the same; what changes is that the constructor is no longer called for literal arguments, so an execute_internal observer no longer sees it. A call to ReflectionProperty::setAccessible() goes from 5,010 to 2,770 instructions, a deprecated user function with literal arguments from 4,640 to 2,520. --- .../functions/argument_binding.phpt | 63 +++++++++++++++++++ .../functions/internal_repeated_calls.phpt | 32 ++++++++++ Zend/tests/attributes/nodiscard/007.phpt | 14 ++++- .../nodiscard/argument_binding.phpt | 41 ++++++++++++ Zend/zend_execute.c | 53 ++++++++++++++++ .../observer_sqlite_create_function.phpt | 3 - 6 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 Zend/tests/attributes/deprecated/functions/argument_binding.phpt create mode 100644 Zend/tests/attributes/deprecated/functions/internal_repeated_calls.phpt create mode 100644 Zend/tests/attributes/nodiscard/argument_binding.phpt diff --git a/Zend/tests/attributes/deprecated/functions/argument_binding.phpt b/Zend/tests/attributes/deprecated/functions/argument_binding.phpt new file mode 100644 index 000000000000..4d89d61388a2 --- /dev/null +++ b/Zend/tests/attributes/deprecated/functions/argument_binding.phpt @@ -0,0 +1,63 @@ +--TEST-- +#[\Deprecated]: positional and named arguments, null, and the errors of the constructor +--FILE-- +getMessage(), "\n"; + } +} + +?> +--EXPECTF-- + +Deprecated: Function positional_message() is deprecated, message only in %s on line %d + +Deprecated: Function named_since() is deprecated since 1.0 in %s on line %d + +Deprecated: Function both_positional() is deprecated since 1.0, m in %s on line %d + +Deprecated: Function named_reversed() is deprecated since 1.0, m in %s on line %d + +Deprecated: Function null_message() is deprecated since 1.0 in %s on line %d + +Deprecated: Function null_since() is deprecated, m in %s on line %d + +Deprecated: Function empty_strings() is deprecated in %s on line %d +Error: Named parameter $message overwrites previous argument +Error: Named parameter $message overwrites previous argument +Error: Unknown named parameter $unknown +Error: Unknown named parameter $Message +ArgumentCountError: Deprecated::__construct() expects at most 2 arguments, 3 given diff --git a/Zend/tests/attributes/deprecated/functions/internal_repeated_calls.phpt b/Zend/tests/attributes/deprecated/functions/internal_repeated_calls.phpt new file mode 100644 index 000000000000..b030b8929a22 --- /dev/null +++ b/Zend/tests/attributes/deprecated/functions/internal_repeated_calls.phpt @@ -0,0 +1,32 @@ +--TEST-- +#[\Deprecated]: the message of an internal function is the same on every call +--FILE-- +setAccessible(true); +$rp->setAccessible(true); +(Closure::fromCallable([$rp, 'setAccessible']))(true); +(new ReflectionMethod($rp, 'setAccessible'))->invoke($rp, true); + +set_error_handler(function (int $type, string $message) { + echo "handler: $message\n"; + return true; +}); +$rp->setAccessible(true); +restore_error_handler(); +$rp->setAccessible(true); + +?> +--EXPECTF-- +Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in %s on line %d + +Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in %s on line %d + +Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in %s on line %d + +Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in %s on line %d +handler: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 + +Deprecated: Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1 in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/007.phpt b/Zend/tests/attributes/nodiscard/007.phpt index de4604e15e48..6d3df246f0aa 100644 --- a/Zend/tests/attributes/nodiscard/007.phpt +++ b/Zend/tests/attributes/nodiscard/007.phpt @@ -7,13 +7,23 @@ zend_test.observer.execute_internal=1 --FILE-- --EXPECTF-- - - + + Warning: The return value of function zend_test_nodiscard() should either be used or intentionally ignored by casting it as (void), custom message in %s on line %d + + + +Warning: The return value of function f() should either be used or intentionally ignored by casting it as (void), from a constant in %s on line %d diff --git a/Zend/tests/attributes/nodiscard/argument_binding.phpt b/Zend/tests/attributes/nodiscard/argument_binding.phpt new file mode 100644 index 000000000000..fc6e9b69c3a3 --- /dev/null +++ b/Zend/tests/attributes/nodiscard/argument_binding.phpt @@ -0,0 +1,41 @@ +--TEST-- +#[\NoDiscard]: positional and named message, null, and the errors of the constructor +--FILE-- +getMessage(), "\n"; + } +} + +?> +--EXPECTF-- + +Warning: The return value of function positional_message() should either be used or intentionally ignored by casting it as (void), positional in %s on line %d + +Warning: The return value of function named_message() should either be used or intentionally ignored by casting it as (void), named in %s on line %d + +Warning: The return value of function null_message() should either be used or intentionally ignored by casting it as (void) in %s on line %d + +Warning: The return value of function empty_message() should either be used or intentionally ignored by casting it as (void) in %s on line %d +Error: Unknown named parameter $since +ArgumentCountError: NoDiscard::__construct() expects at most 1 argument, 2 given +Error: Unknown named parameter $unknown diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index e7923d76996c..de943385c536 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1831,6 +1831,48 @@ ZEND_API zend_never_inline ZEND_COLD void zend_wrong_string_offset_error(void) zend_throw_error(NULL, "%s", msg); } +/* Reads the arguments of a #[\Deprecated] (message, since) or #[\NoDiscard] (message) + * attribute when they are strings or null, positional or named, each at most once; anything + * else is left to the constructor and its errors */ +static bool get_attribute_string_args(const zend_attribute *attr, zend_string **message, zend_string **since) +{ + zend_string *strs[2] = {NULL, NULL}; + uint32_t num_params = since ? 2 : 1; + + if (attr->argc > num_params) { + return false; + } + for (uint32_t i = 0; i < attr->argc; i++) { + const zend_attribute_arg *arg = &attr->args[i]; + uint32_t param; + + if (arg->name == NULL) { + param = i; + } else if (zend_string_equals(arg->name, ZSTR_KNOWN(ZEND_STR_MESSAGE))) { + param = 0; + } else if (num_params == 2 && zend_string_equals(arg->name, ZSTR_KNOWN(ZEND_STR_SINCE))) { + param = 1; + } else { + return false; + } + if (strs[param] != NULL) { + return false; + } + if (Z_TYPE(arg->value) == IS_STRING) { + strs[param] = Z_STR(arg->value); + } else if (Z_TYPE(arg->value) == IS_NULL) { + strs[param] = ZSTR_EMPTY_ALLOC(); + } else { + return false; + } + } + *message = strs[0] ? strs[0] : ZSTR_EMPTY_ALLOC(); + if (since) { + *since = strs[1] ? strs[1] : ZSTR_EMPTY_ALLOC(); + } + return true; +} + ZEND_COLD static zend_result ZEND_FASTCALL get_deprecation_suffix_from_attribute(HashTable *attributes, zend_class_entry* scope, zend_string **message_suffix) { *message_suffix = ZSTR_EMPTY_ALLOC(); @@ -1858,6 +1900,11 @@ ZEND_COLD static zend_result ZEND_FASTCALL get_deprecation_suffix_from_attribute ZVAL_UNDEF(&obj); zval *z; + /* With an exception pending the constructor is not run and nothing is emitted; keep that */ + if (!EG(exception) && get_attribute_string_args(deprecated, &message, &since)) { + goto build; + } + /* Construct the Deprecated object to correctly handle parameter processing. */ if (FAILURE == zend_get_attribute_object(&obj, zend_ce_deprecated, deprecated, scope, NULL)) { goto out; @@ -1877,6 +1924,7 @@ ZEND_COLD static zend_result ZEND_FASTCALL get_deprecation_suffix_from_attribute since = Z_STR_P(z); } +build: /* Construct the suffix. */ *message_suffix = zend_strpprintf_unchecked( 0, @@ -1948,6 +1996,10 @@ ZEND_COLD static zend_result ZEND_FASTCALL get_nodiscard_suffix_from_attribute(H ZVAL_UNDEF(&obj); zval *z; + if (!EG(exception) && get_attribute_string_args(nodiscard, &message, NULL)) { + goto build; + } + /* Construct the NoDiscard object to correctly handle parameter processing. */ if (FAILURE == zend_get_attribute_object(&obj, zend_ce_nodiscard, nodiscard, scope, NULL)) { goto out; @@ -1960,6 +2012,7 @@ ZEND_COLD static zend_result ZEND_FASTCALL get_nodiscard_suffix_from_attribute(H message = Z_STR_P(z); } +build: /* Construct the suffix. */ *message_suffix = zend_strpprintf_unchecked( 0, diff --git a/ext/zend_test/tests/observer_sqlite_create_function.phpt b/ext/zend_test/tests/observer_sqlite_create_function.phpt index 5d809056193d..fa39d18cd162 100644 --- a/ext/zend_test/tests/observer_sqlite_create_function.phpt +++ b/ext/zend_test/tests/observer_sqlite_create_function.phpt @@ -30,9 +30,6 @@ echo 'Done' . PHP_EOL; - - - Deprecated: Method PDO::sqliteCreateFunction() is deprecated since 8.5, use Pdo\Sqlite::createFunction() instead in %s on line %d