From b5c39e575c507a2524a6eeb15f9c27dcc051b9b3 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sat, 5 Jul 2025 15:47:54 -0700 Subject: [PATCH] [RFC] Allow `#[\Deprecated]` on traits --- .../constant_listed_as_target-internal.phpt | 6 ++-- .../attributes/deprecated/traits/basic.phpt | 15 +++++++++ .../deprecated/traits/class_not_trait.phpt | 11 +++++++ .../deprecated/traits/enum_not_trait.phpt | 11 +++++++ .../deprecated/traits/exception.phpt | 25 ++++++++++++++ .../deprecated/traits/inheritance.phpt | 18 ++++++++++ .../traits/interface_not_trait.phpt | 11 +++++++ .../deprecated/traits/messages.phpt | 33 +++++++++++++++++++ Zend/zend_attributes.c | 29 ++++++++++++++++ Zend/zend_attributes.stub.php | 2 +- Zend/zend_attributes_arginfo.h | 4 +-- Zend/zend_compile.h | 10 +++--- Zend/zend_execute.c | 21 ++++++++++++ Zend/zend_execute.h | 1 + Zend/zend_inheritance.c | 7 ++++ 15 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 Zend/tests/attributes/deprecated/traits/basic.phpt create mode 100644 Zend/tests/attributes/deprecated/traits/class_not_trait.phpt create mode 100644 Zend/tests/attributes/deprecated/traits/enum_not_trait.phpt create mode 100644 Zend/tests/attributes/deprecated/traits/exception.phpt create mode 100644 Zend/tests/attributes/deprecated/traits/inheritance.phpt create mode 100644 Zend/tests/attributes/deprecated/traits/interface_not_trait.phpt create mode 100644 Zend/tests/attributes/deprecated/traits/messages.phpt diff --git a/Zend/tests/attributes/constants/constant_listed_as_target-internal.phpt b/Zend/tests/attributes/constants/constant_listed_as_target-internal.phpt index b0b88c2f6edab..1a64c2bada422 100644 --- a/Zend/tests/attributes/constants/constant_listed_as_target-internal.phpt +++ b/Zend/tests/attributes/constants/constant_listed_as_target-internal.phpt @@ -4,8 +4,10 @@ Constants listed in valid targets when used wrong (internal attribute) --EXPECTF-- -Fatal error: Attribute "Deprecated" cannot target class (allowed targets: function, method, class constant, constant) in %s on line %d +Fatal error: Attribute "Deprecated" cannot target parameter (allowed targets: class, function, method, class constant, constant) in %s on line %d diff --git a/Zend/tests/attributes/deprecated/traits/basic.phpt b/Zend/tests/attributes/deprecated/traits/basic.phpt new file mode 100644 index 0000000000000..412cd6f80ed10 --- /dev/null +++ b/Zend/tests/attributes/deprecated/traits/basic.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\Deprecated]: Basic trait deprecation +--FILE-- + +--EXPECTF-- +Deprecated: Trait DemoTrait used by DemoClass is deprecated in %s on line %d diff --git a/Zend/tests/attributes/deprecated/traits/class_not_trait.phpt b/Zend/tests/attributes/deprecated/traits/class_not_trait.phpt new file mode 100644 index 0000000000000..fea51b3dd3c02 --- /dev/null +++ b/Zend/tests/attributes/deprecated/traits/class_not_trait.phpt @@ -0,0 +1,11 @@ +--TEST-- +#[\Deprecated]: Using on a class +--FILE-- + +--EXPECTF-- +Fatal error: Cannot apply #[Deprecated] to class Demo in %s on line %d diff --git a/Zend/tests/attributes/deprecated/traits/enum_not_trait.phpt b/Zend/tests/attributes/deprecated/traits/enum_not_trait.phpt new file mode 100644 index 0000000000000..3d4d47be5fda5 --- /dev/null +++ b/Zend/tests/attributes/deprecated/traits/enum_not_trait.phpt @@ -0,0 +1,11 @@ +--TEST-- +#[\Deprecated]: Using on an enum +--FILE-- + +--EXPECTF-- +Fatal error: Cannot apply #[Deprecated] to enum Demo in %s on line %d diff --git a/Zend/tests/attributes/deprecated/traits/exception.phpt b/Zend/tests/attributes/deprecated/traits/exception.phpt new file mode 100644 index 0000000000000..2704ea68e5b3a --- /dev/null +++ b/Zend/tests/attributes/deprecated/traits/exception.phpt @@ -0,0 +1,25 @@ +--TEST-- +#[\Deprecated]: Deprecation converted to ErrorException does not break +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught ErrorException: Trait DemoTrait used by DemoClass is deprecated in %s:%d +Stack trace: +#0 %s: my_error_handler(16384, 'Trait DemoTrait...', '%s', %d) +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/attributes/deprecated/traits/inheritance.phpt b/Zend/tests/attributes/deprecated/traits/inheritance.phpt new file mode 100644 index 0000000000000..6a54a83e2e440 --- /dev/null +++ b/Zend/tests/attributes/deprecated/traits/inheritance.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\Deprecated]: Deprecated traits only apply to direct use, not inheritance +--FILE-- + +--EXPECTF-- +Deprecated: Trait DemoTrait used by DemoClass is deprecated in %s on line %d diff --git a/Zend/tests/attributes/deprecated/traits/interface_not_trait.phpt b/Zend/tests/attributes/deprecated/traits/interface_not_trait.phpt new file mode 100644 index 0000000000000..d49ec9ced76e2 --- /dev/null +++ b/Zend/tests/attributes/deprecated/traits/interface_not_trait.phpt @@ -0,0 +1,11 @@ +--TEST-- +#[\Deprecated]: Using on an interface +--FILE-- + +--EXPECTF-- +Fatal error: Cannot apply #[Deprecated] to interface Demo in %s on line %d diff --git a/Zend/tests/attributes/deprecated/traits/messages.phpt b/Zend/tests/attributes/deprecated/traits/messages.phpt new file mode 100644 index 0000000000000..f57d8eb891a3b --- /dev/null +++ b/Zend/tests/attributes/deprecated/traits/messages.phpt @@ -0,0 +1,33 @@ +--TEST-- +#[\Deprecated]: Messages for deprecated traits +--FILE-- + +--EXPECTF-- +Deprecated: Trait DemoTrait1 used by DemoClass is deprecated, please do not use in %s on line %d + +Deprecated: Trait DemoTrait2 used by DemoClass is deprecated since 2.7, will be removed in 3.0 in %s on line %d + +Deprecated: Trait DemoTrait3 used by DemoClass is deprecated, going away in %s on line %d + +Deprecated: Trait DemoTrait4 used by DemoClass is deprecated since 3.5 in %s on line %d diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index c3633801be83e..59cd4ce620bb7 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -95,6 +95,34 @@ static void validate_allow_dynamic_properties( scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES; } +static void validate_deprecated( + zend_attribute *attr, + uint32_t target, + zend_class_entry *scope +) { + if (target != ZEND_ATTRIBUTE_TARGET_CLASS) { + // Being used for a method or something, validation does not apply + return; + } + if (scope->ce_flags & ZEND_ACC_TRAIT) { + scope->ce_flags |= ZEND_ACC_DEPRECATED; + return; + } + + const char *type = "class"; + if (scope->ce_flags & ZEND_ACC_INTERFACE) { + type = "interface"; + } else if (scope->ce_flags & ZEND_ACC_ENUM) { + type = "enum"; + } + zend_error_noreturn( + E_ERROR, + "Cannot apply #[Deprecated] to %s %s", + type, + ZSTR_VAL(scope->name) + ); +} + ZEND_METHOD(Attribute, __construct) { zend_long flags = ZEND_ATTRIBUTE_TARGET_ALL; @@ -545,6 +573,7 @@ void zend_register_attribute_ce(void) zend_ce_deprecated = register_class_Deprecated(); attr = zend_mark_internal_attribute(zend_ce_deprecated); + attr->validator = validate_deprecated; zend_ce_nodiscard = register_class_NoDiscard(); attr = zend_mark_internal_attribute(zend_ce_nodiscard); diff --git a/Zend/zend_attributes.stub.php b/Zend/zend_attributes.stub.php index fe70de83e4d21..826ffbe558c53 100644 --- a/Zend/zend_attributes.stub.php +++ b/Zend/zend_attributes.stub.php @@ -77,7 +77,7 @@ public function __construct() {} /** * @strict-properties */ -#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION|Attribute::TARGET_CLASS_CONSTANT|Attribute::TARGET_CONSTANT)] +#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION|Attribute::TARGET_CLASS_CONSTANT|Attribute::TARGET_CONSTANT|Attribute::TARGET_CLASS)] final class Deprecated { public readonly ?string $message; diff --git a/Zend/zend_attributes_arginfo.h b/Zend/zend_attributes_arginfo.h index 14afe40c01adf..ef5907853aabe 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: 9aee3d8f2ced376f5929048444eaa2529ff90311 */ + * Stub hash: caf2252406298eef71052ced0b344a63b4f36fe7 */ 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") @@ -264,7 +264,7 @@ static zend_class_entry *register_class_Deprecated(void) zend_attribute *attribute_Attribute_class_Deprecated_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_Deprecated_0, 1); zend_string_release(attribute_name_Attribute_class_Deprecated_0); zval attribute_Attribute_class_Deprecated_0_arg0; - ZVAL_LONG(&attribute_Attribute_class_Deprecated_0_arg0, ZEND_ATTRIBUTE_TARGET_METHOD | ZEND_ATTRIBUTE_TARGET_FUNCTION | ZEND_ATTRIBUTE_TARGET_CLASS_CONST | ZEND_ATTRIBUTE_TARGET_CONST); + ZVAL_LONG(&attribute_Attribute_class_Deprecated_0_arg0, ZEND_ATTRIBUTE_TARGET_METHOD | ZEND_ATTRIBUTE_TARGET_FUNCTION | ZEND_ATTRIBUTE_TARGET_CLASS_CONST | ZEND_ATTRIBUTE_TARGET_CONST | ZEND_ATTRIBUTE_TARGET_CLASS); ZVAL_COPY_VALUE(&attribute_Attribute_class_Deprecated_0->args[0].value, &attribute_Attribute_class_Deprecated_0_arg0); return class_entry; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 7ac0a2b8b2c44..9cba63f2bcf16 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -253,6 +253,9 @@ typedef struct _zend_oparray_context { /* or IS_CONSTANT_VISITED_MARK | | | */ #define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */ /* | | | */ +/* deprecation flag | | | */ +#define ZEND_ACC_DEPRECATED (1 << 11) /* X | X | | X */ +/* | | | */ /* Property Flags (unused: 13...) | | | */ /* =========== | | | */ /* | | | */ @@ -267,7 +270,7 @@ typedef struct _zend_oparray_context { #define ZEND_ACC_PROTECTED_SET (1 << 11) /* | | X | */ #define ZEND_ACC_PRIVATE_SET (1 << 12) /* | | X | */ /* | | | */ -/* Class Flags (unused: 30,31) | | | */ +/* Class Flags (unused: 31) | | | */ /* =========== | | | */ /* | | | */ /* Special class types | | | */ @@ -285,7 +288,7 @@ typedef struct _zend_oparray_context { /* | | | */ /* Class has magic methods __get/__set/__unset/ | | | */ /* __isset that use guards | | | */ -#define ZEND_ACC_USE_GUARDS (1 << 11) /* X | | | */ +#define ZEND_ACC_USE_GUARDS (1 << 30) /* X | | | */ /* | | | */ /* Class constants updated | | | */ #define ZEND_ACC_CONSTANTS_UPDATED (1 << 12) /* X | | | */ @@ -336,9 +339,6 @@ typedef struct _zend_oparray_context { /* Function Flags (unused: 30) | | | */ /* ============== | | | */ /* | | | */ -/* deprecation flag | | | */ -#define ZEND_ACC_DEPRECATED (1 << 11) /* | X | | X */ -/* | | | */ /* Function returning by reference | | | */ #define ZEND_ACC_RETURN_REFERENCE (1 << 12) /* | X | | */ /* | | | */ diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index e1593cadce7d0..1f4daa6e087cd 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2006,6 +2006,27 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_constant(const zend_consta zend_string_release(message_suffix); } +ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_trait( + zend_class_entry *trait, + const zend_string *used_by +) { + zend_string *message_suffix = ZSTR_EMPTY_ALLOC(); + + if (get_deprecation_suffix_from_attribute(trait->attributes, trait, &message_suffix) == FAILURE) { + return; + } + + int code = trait->type == ZEND_INTERNAL_CLASS ? E_DEPRECATED : E_USER_DEPRECATED; + + zend_error_unchecked(code, "Trait %s used by %s is deprecated%S", + ZSTR_VAL(trait->name), + ZSTR_VAL(used_by), + message_suffix + ); + + zend_string_release(message_suffix); +} + ZEND_API ZEND_COLD void ZEND_FASTCALL zend_false_to_array_deprecated(void) { zend_error(E_DEPRECATED, "Automatic conversion of false to array is deprecated"); diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index cf15c9e3b2db5..8328cc95728e0 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -66,6 +66,7 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_function(const zend_functi 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_deprecated_constant(const zend_constant *c, const zend_string *constant_name); +ZEND_API ZEND_COLD void ZEND_FASTCALL zend_deprecated_trait(zend_class_entry *trait, const zend_string *used_by); 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); ZEND_API ZEND_COLD void ZEND_FASTCALL zend_use_resource_as_offset(const zval *dim); diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index d27cca5b76187..ac4adbbdb956e 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -3507,6 +3507,13 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string free_alloca(traits_and_interfaces, use_heap); return NULL; } + if (UNEXPECTED(trait->ce_flags & ZEND_ACC_DEPRECATED)) { + zend_deprecated_trait(trait, ce->name); + if (UNEXPECTED(EG(exception))) { + free_alloca(traits_and_interfaces, use_heap); + return NULL; + } + } for (j = 0; j < i; j++) { if (traits_and_interfaces[j] == trait) { /* skip duplications */