From 7d864d5a0485bc0c6c1c1aea6d3d70e34493cb78 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 9 Jun 2025 13:24:57 -0700 Subject: [PATCH 1/9] Add `#[\DelayedTargetValidation]` attribute --- .../has_runtime_errors.phpt | 225 ++++++++++++++++++ .../no_compile_errors.phpt | 46 ++++ .../repetition_errors.phpt | 13 + Zend/zend_attributes.c | 41 ++-- Zend/zend_attributes.h | 5 + Zend/zend_attributes.stub.php | 6 + Zend/zend_attributes_arginfo.h | 16 +- Zend/zend_compile.c | 23 +- ext/reflection/php_reflection.c | 46 ++-- 9 files changed, 380 insertions(+), 41 deletions(-) create mode 100644 Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/repetition_errors.phpt diff --git a/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt new file mode 100644 index 0000000000000..0d280bd446159 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt @@ -0,0 +1,225 @@ +--TEST-- +#[\DelayedTargetValidation] prevents target errors at compile time +--FILE-- +v = $v2; + echo __METHOD__ . "\n"; + } +} + +#[DelayedTargetValidation] +#[Attribute] +function demoFn() { + echo __FUNCTION__ . "\n"; +} + +#[DelayedTargetValidation] +#[Attribute] +const EXAMPLE = true; + +$cases = [ + new ReflectionClass('Demo'), + new ReflectionClassConstant('Demo', 'FOO'), + new ReflectionProperty('Demo', 'v'), + new ReflectionMethod('Demo', '__construct'), + new ReflectionParameter([ 'Demo', '__construct' ], 'v2'), + new ReflectionProperty('Demo', 'v2'), + new ReflectionFunction('demoFn'), + new ReflectionConstant('EXAMPLE'), +]; +foreach ($cases as $r) { + echo str_repeat("*", 20) . "\n"; + echo $r . "\n"; + $attributes = $r->getAttributes(); + var_dump($attributes); + try { + $attributes[1]->newInstance(); + } catch (Error $e) { + echo get_class($e) . ": " . $e->getMessage() . "\n"; + } +} + +?> +--EXPECTF-- +******************** +Class [ class Demo ] { + @@ %s %d-%d + + - Constants [1] { + Constant [ public string FOO ] { BAR } + } + + - Static properties [0] { + } + + - Static methods [0] { + } + + - Properties [2] { + Property [ public string $v ] + Property [ public string $v2 ] + } + + - Methods [1] { + Method [ public method __construct ] { + @@ %s %d - %d + + - Parameters [1] { + Parameter #0 [ string $v2 ] + } + } + } +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "NoDiscard" + } +} +Error: Attribute "NoDiscard" cannot target class (allowed targets: function, method) +******************** +Constant [ public string FOO ] { BAR } + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target class constant (allowed targets: class) +******************** +Property [ public string $v ] + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target property (allowed targets: class) +******************** +Method [ public method __construct ] { + @@ %s %d - %d + + - Parameters [1] { + Parameter #0 [ string $v2 ] + } +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target method (allowed targets: class) +******************** +Parameter #0 [ string $v2 ] +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target parameter (allowed targets: class) +******************** +Property [ public string $v2 ] + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target property (allowed targets: class) +******************** +Function [ function demoFn ] { + @@ %s %d - %d +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target function (allowed targets: class) +******************** +Constant [ bool EXAMPLE ] { 1 } + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target constant (allowed targets: class) diff --git a/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt b/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt new file mode 100644 index 0000000000000..ea96a069d748c --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt @@ -0,0 +1,46 @@ +--TEST-- +#[\DelayedTargetValidation] prevents target errors at compile time +--FILE-- +v = $v2; + echo __METHOD__ . "\n"; + } +} + +#[DelayedTargetValidation] +#[Attribute] +function demoFn() { + echo __FUNCTION__ . "\n"; +} + +$o = new Demo( "foo" ); +demoFn(); + +#[DelayedTargetValidation] +#[Attribute] +const EXAMPLE = true; + +?> +--EXPECT-- +Demo::__construct +demoFn diff --git a/Zend/tests/attributes/delayed_target_validation/repetition_errors.phpt b/Zend/tests/attributes/delayed_target_validation/repetition_errors.phpt new file mode 100644 index 0000000000000..5c8f9bfc9dde2 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/repetition_errors.phpt @@ -0,0 +1,13 @@ +--TEST-- +#[\DelayedTargetValidation] does not prevent repetition errors +--FILE-- + +--EXPECTF-- +Fatal error: Attribute "NoDiscard" must not be repeated in %s on line %d diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index 01b16d7d205eb..10c7f2c932c93 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -32,6 +32,7 @@ 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; +ZEND_API zend_class_entry *zend_ce_delayed_target_validation; static zend_object_handlers attributes_object_handlers_sensitive_parameter_value; @@ -72,25 +73,21 @@ uint32_t zend_attribute_attribute_get_flags(zend_attribute *attr, zend_class_ent static void validate_allow_dynamic_properties( zend_attribute *attr, uint32_t target, zend_class_entry *scope) { + const char *msg = NULL; if (scope->ce_flags & ZEND_ACC_TRAIT) { - zend_error_noreturn(E_ERROR, "Cannot apply #[\\AllowDynamicProperties] to trait %s", - ZSTR_VAL(scope->name) - ); + msg = "Cannot apply #[\\AllowDynamicProperties] to trait %s"; + } else if (scope->ce_flags & ZEND_ACC_INTERFACE) { + msg = "Cannot apply #[\\AllowDynamicProperties] to interface %s"; + } else if (scope->ce_flags & ZEND_ACC_READONLY_CLASS) { + msg = "Cannot apply #[\\AllowDynamicProperties] to readonly class %s"; + } else if (scope->ce_flags & ZEND_ACC_ENUM) { + msg = "Cannot apply #[\\AllowDynamicProperties] to enum %s"; } - if (scope->ce_flags & ZEND_ACC_INTERFACE) { - zend_error_noreturn(E_ERROR, "Cannot apply #[\\AllowDynamicProperties] to interface %s", - ZSTR_VAL(scope->name) - ); - } - if (scope->ce_flags & ZEND_ACC_READONLY_CLASS) { - zend_error_noreturn(E_ERROR, "Cannot apply #[\\AllowDynamicProperties] to readonly class %s", - ZSTR_VAL(scope->name) - ); - } - if (scope->ce_flags & ZEND_ACC_ENUM) { - zend_error_noreturn(E_ERROR, "Cannot apply #[\\AllowDynamicProperties] to enum %s", - ZSTR_VAL(scope->name) - ); + if (msg != NULL) { + if (target & ZEND_ATTRIBUTE_NO_TARGET_VALIDATION) { + return; + } + zend_error_noreturn(E_ERROR, msg, ZSTR_VAL(scope->name) ); } scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES; } @@ -487,7 +484,12 @@ ZEND_API zend_internal_attribute *zend_mark_internal_attribute(zend_class_entry if (zend_string_equals(attr->name, zend_ce_attribute->name)) { internal_attr = pemalloc(sizeof(zend_internal_attribute), 1); internal_attr->ce = ce; - internal_attr->flags = Z_LVAL(attr->args[0].value); + if (Z_TYPE(attr->args[0].value) == IS_NULL) { + // Apply default of Attribute::TARGET_ALL + internal_attr->flags = ZEND_ATTRIBUTE_TARGET_ALL; + } else { + internal_attr->flags = Z_LVAL(attr->args[0].value); + } internal_attr->validator = NULL; zend_string *lcname = zend_string_tolower_ex(ce->name, 1); @@ -548,6 +550,9 @@ void zend_register_attribute_ce(void) zend_ce_nodiscard = register_class_NoDiscard(); attr = zend_mark_internal_attribute(zend_ce_nodiscard); + + zend_ce_delayed_target_validation = register_class_DelayedTargetValidation(); + attr = zend_mark_internal_attribute(zend_ce_delayed_target_validation); } void zend_attributes_shutdown(void) diff --git a/Zend/zend_attributes.h b/Zend/zend_attributes.h index a4d6b28c0094a..792b0d23e7a23 100644 --- a/Zend/zend_attributes.h +++ b/Zend/zend_attributes.h @@ -34,6 +34,10 @@ #define ZEND_ATTRIBUTE_IS_REPEATABLE (1<<7) #define ZEND_ATTRIBUTE_FLAGS ((1<<8) - 1) +/* Not a real flag, just passed to validators when target validation is * + * suppressed; must not conflict with any of the real flags above. */ +#define ZEND_ATTRIBUTE_NO_TARGET_VALIDATION (1<<8) + /* Flags for zend_attribute.flags */ #define ZEND_ATTRIBUTE_PERSISTENT (1<<0) #define ZEND_ATTRIBUTE_STRICT_TYPES (1<<1) @@ -50,6 +54,7 @@ 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; +extern ZEND_API zend_class_entry *zend_ce_delayed_target_validation; typedef struct { zend_string *name; diff --git a/Zend/zend_attributes.stub.php b/Zend/zend_attributes.stub.php index fe70de83e4d21..e649c24d44579 100644 --- a/Zend/zend_attributes.stub.php +++ b/Zend/zend_attributes.stub.php @@ -97,3 +97,9 @@ final class NoDiscard public function __construct(?string $message = null) {} } + +/** + * @strict-properties + */ +#[Attribute] +final class DelayedTargetValidation {} diff --git a/Zend/zend_attributes_arginfo.h b/Zend/zend_attributes_arginfo.h index ce97495b7eaf7..1ebaba3e8f6ce 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: ce91f65ef2a10f75c5d8f3ef309a4b09bda6b827 */ 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") @@ -276,3 +276,17 @@ static zend_class_entry *register_class_NoDiscard(void) return class_entry; } + +static zend_class_entry *register_class_DelayedTargetValidation(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "DelayedTargetValidation", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES); + + zend_string *attribute_name_Attribute_class_DelayedTargetValidation_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, 1); + zend_add_class_attribute(class_entry, attribute_name_Attribute_class_DelayedTargetValidation_0, 0); + zend_string_release(attribute_name_Attribute_class_DelayedTargetValidation_0); + + return class_entry; +} diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index eb2286b932959..af8ce4dc405da 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -7543,19 +7543,28 @@ static void zend_compile_attributes( } if (*attributes != NULL) { + /* Allow delaying target validation for forward compatibility. */ + zend_attribute *delayed_target_validation = zend_get_attribute_str( + *attributes, + "delayedtargetvalidation", + strlen("delayedtargetvalidation") + ); + uint32_t extra_flags = delayed_target_validation ? ZEND_ATTRIBUTE_NO_TARGET_VALIDATION : 0; /* Validate attributes in a secondary loop (needed to detect repeated attributes). */ ZEND_HASH_PACKED_FOREACH_PTR(*attributes, attr) { if (attr->offset != offset || NULL == (config = zend_internal_attribute_get(attr->lcname))) { continue; } - if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) { - zend_string *location = zend_get_attribute_target_names(target); - zend_string *allowed = zend_get_attribute_target_names(config->flags); + if (delayed_target_validation == NULL) { + if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) { + zend_string *location = zend_get_attribute_target_names(target); + zend_string *allowed = zend_get_attribute_target_names(config->flags); - zend_error_noreturn(E_ERROR, "Attribute \"%s\" cannot target %s (allowed targets: %s)", - ZSTR_VAL(attr->name), ZSTR_VAL(location), ZSTR_VAL(allowed) - ); + zend_error_noreturn(E_ERROR, "Attribute \"%s\" cannot target %s (allowed targets: %s)", + ZSTR_VAL(attr->name), ZSTR_VAL(location), ZSTR_VAL(allowed) + ); + } } if (!(config->flags & ZEND_ATTRIBUTE_IS_REPEATABLE)) { @@ -7565,7 +7574,7 @@ static void zend_compile_attributes( } if (config->validator != NULL) { - config->validator(attr, target, CG(active_class_entry)); + config->validator(attr, target | extra_flags, CG(active_class_entry)); } } ZEND_HASH_FOREACH_END(); } diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index bc9f06e5e1a3c..540b38e68377b 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7305,26 +7305,42 @@ ZEND_METHOD(ReflectionAttribute, newInstance) RETURN_THROWS(); } - if (ce->type == ZEND_USER_CLASS) { - uint32_t flags = zend_attribute_attribute_get_flags(marker, ce); - if (EG(exception)) { - RETURN_THROWS(); - } + /* This code can be reached under one of three possible conditions: + * - the attribute is an internal attribute, and it had the target and + * and repetition validated already + * - the attribute is an internal attribute and repetition was validated + * already, but the target was not validated due to the presence of + * #[DelayedTargetValidation] + * - the attribute is a user attribute, and neither target nor repetition + * have been validated. + * + * It is not worth checking for the presence of #[DelayedTargetValidation] + * to determine if we should run target validation for internal attributes; + * it is faster just to do the validation, which will always pass if the + * attribute is absent. + */ + uint32_t flags = zend_attribute_attribute_get_flags(marker, ce); + if (EG(exception)) { + RETURN_THROWS(); + } - if (!(attr->target & flags)) { - zend_string *location = zend_get_attribute_target_names(attr->target); - zend_string *allowed = zend_get_attribute_target_names(flags); + if (!(attr->target & flags)) { + zend_string *location = zend_get_attribute_target_names(attr->target); + zend_string *allowed = zend_get_attribute_target_names(flags); - zend_throw_error(NULL, "Attribute \"%s\" cannot target %s (allowed targets: %s)", - ZSTR_VAL(attr->data->name), ZSTR_VAL(location), ZSTR_VAL(allowed) - ); + zend_throw_error(NULL, "Attribute \"%s\" cannot target %s (allowed targets: %s)", + ZSTR_VAL(attr->data->name), ZSTR_VAL(location), ZSTR_VAL(allowed) + ); - zend_string_release(location); - zend_string_release(allowed); + zend_string_release(location); + zend_string_release(allowed); - RETURN_THROWS(); - } + RETURN_THROWS(); + } + /* Repetition validation is done even if #[DelayedTargetValidation] is used + * and so can be skipped for internal attributes. */ + if (ce->type == ZEND_USER_CLASS) { if (!(flags & ZEND_ATTRIBUTE_IS_REPEATABLE)) { if (zend_is_attribute_repeated(attr->attributes, attr->data)) { zend_throw_error(NULL, "Attribute \"%s\" must not be repeated", ZSTR_VAL(attr->data->name)); From fc5194c06dfaf510e5e3c0a8c13ea669124a88d6 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 9 Jun 2025 13:44:00 -0700 Subject: [PATCH 2/9] Avoid uninitialized memory --- Zend/zend_attributes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index 10c7f2c932c93..ec6fe66db763f 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -484,7 +484,7 @@ ZEND_API zend_internal_attribute *zend_mark_internal_attribute(zend_class_entry if (zend_string_equals(attr->name, zend_ce_attribute->name)) { internal_attr = pemalloc(sizeof(zend_internal_attribute), 1); internal_attr->ce = ce; - if (Z_TYPE(attr->args[0].value) == IS_NULL) { + if (attr->argc == 0) { // Apply default of Attribute::TARGET_ALL internal_attr->flags = ZEND_ATTRIBUTE_TARGET_ALL; } else { From 0df2188e8103dd191c135a12171744727b78cf81 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sun, 6 Jul 2025 10:30:02 -0700 Subject: [PATCH 3/9] Run validators for delayed validators --- .../errors_from_validator.phpt | 180 ++++++++++++++++++ .../has_runtime_errors.phpt | 2 +- .../validator_success.phpt | 62 ++++++ Zend/zend_attributes.c | 12 ++ Zend/zend_attributes.h | 6 +- ext/reflection/php_reflection.c | 15 ++ 6 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/validator_success.phpt diff --git a/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt b/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt new file mode 100644 index 0000000000000..d0948e5fd7ad0 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt @@ -0,0 +1,180 @@ +--TEST-- +#[\DelayedTargetValidation] affects errors from validators +--FILE-- +getAttributes(); + var_dump($attributes); + try { + $attributes[1]->newInstance(); + } catch (Error $e) { + echo get_class($e) . ": " . $e->getMessage() . "\n"; + } +} + +?> +--EXPECTF-- +******************** +Trait [ trait DemoTrait ] { + @@ %s %d-%d + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [0] { + } + + - Properties [0] { + } + + - Methods [0] { + } +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(22) "AllowDynamicProperties" + } +} +Error: Cannot apply #[AllowDynamicProperties] to trait DemoTrait +******************** +Interface [ interface DemoInterface ] { + @@ %s %d-%d + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [0] { + } + + - Properties [0] { + } + + - Methods [0] { + } +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(22) "AllowDynamicProperties" + } +} +Error: Cannot apply #[AllowDynamicProperties] to interface DemoInterface +******************** +Class [ readonly class DemoReadonly ] { + @@ %s %d-%d + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [0] { + } + + - Properties [0] { + } + + - Methods [0] { + } +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(22) "AllowDynamicProperties" + } +} +Error: Cannot apply #[AllowDynamicProperties] to readonly class DemoReadonly +******************** +Enum [ enum DemoEnum implements UnitEnum ] { + @@ %s %d-%d + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [1] { + Method [ static public method cases ] { + + - Parameters [0] { + } + - Return [ array ] + } + } + + - Properties [1] { + Property [ public protected(set) readonly string $name ] + } + + - Methods [0] { + } +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(22) "AllowDynamicProperties" + } +} +Error: Cannot apply #[AllowDynamicProperties] to enum DemoEnum diff --git a/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt index 0d280bd446159..bb65b126172fb 100644 --- a/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt +++ b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt @@ -1,5 +1,5 @@ --TEST-- -#[\DelayedTargetValidation] prevents target errors at compile time +#[\DelayedTargetValidation] has errors at runtime --FILE-- dynamic = true; +var_dump($obj); + +$ref = new ReflectionClass('DemoClass'); +echo $ref . "\n"; +$attributes = $ref->getAttributes(); +var_dump($attributes); +var_dump($attributes[1]->newInstance()); + +?> +--EXPECTF-- +object(DemoClass)#%d (0) { +} +object(DemoClass)#%d (1) { + ["dynamic"]=> + bool(true) +} +Class [ class DemoClass ] { + @@ %s %d-%d + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [0] { + } + + - Properties [0] { + } + + - Methods [0] { + } +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(22) "AllowDynamicProperties" + } +} +object(AllowDynamicProperties)#%d (0) { +} diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index ec6fe66db763f..5e7a9d3d6e71b 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -87,8 +87,20 @@ static void validate_allow_dynamic_properties( if (target & ZEND_ATTRIBUTE_NO_TARGET_VALIDATION) { return; } + if (target & ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION) { + // Should not have passed the first time + ZEND_ASSERT((scope->ce_flags & ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES) == 0); + // Throw a catchable error at runtime + zend_throw_error(NULL, msg, ZSTR_VAL(scope->name)); + return; + } zend_error_noreturn(E_ERROR, msg, ZSTR_VAL(scope->name) ); } + if (target & ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION) { + // Should have passed the first time + ZEND_ASSERT((scope->ce_flags & ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES) != 0); + return; + } scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES; } diff --git a/Zend/zend_attributes.h b/Zend/zend_attributes.h index 792b0d23e7a23..08f199c7cd52a 100644 --- a/Zend/zend_attributes.h +++ b/Zend/zend_attributes.h @@ -34,10 +34,14 @@ #define ZEND_ATTRIBUTE_IS_REPEATABLE (1<<7) #define ZEND_ATTRIBUTE_FLAGS ((1<<8) - 1) -/* Not a real flag, just passed to validators when target validation is * +/* Not a real flag, just passed to validators when target validation is * suppressed; must not conflict with any of the real flags above. */ #define ZEND_ATTRIBUTE_NO_TARGET_VALIDATION (1<<8) +/* Not a real flag, just passed to validators when target validation is + * being run at runtime; must not conflict with any of the real flags above. */ +#define ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION (1<<9) + /* Flags for zend_attribute.flags */ #define ZEND_ATTRIBUTE_PERSISTENT (1<<0) #define ZEND_ATTRIBUTE_STRICT_TYPES (1<<1) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 540b38e68377b..63b37dcd59496 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7338,6 +7338,21 @@ ZEND_METHOD(ReflectionAttribute, newInstance) RETURN_THROWS(); } + /* Run the delayed validator function for internal attributes */ + if (ce->type == ZEND_INTERNAL_CLASS) { + zend_internal_attribute *config = zend_internal_attribute_get(attr->data->lcname); + if (config != NULL && config->validator != NULL) { + config->validator( + attr->data, + attr->target | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION, + attr->scope + ); + if (EG(exception)) { + RETURN_THROWS(); + } + } + } + /* Repetition validation is done even if #[DelayedTargetValidation] is used * and so can be skipped for internal attributes. */ if (ce->type == ZEND_USER_CLASS) { From 4e71690bf82329199b1bd2b86f074ae25f7a0296 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sun, 6 Jul 2025 11:06:45 -0700 Subject: [PATCH 4/9] Fix for reflection --- ext/reflection/php_reflection.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 63b37dcd59496..300f9fc3b59d2 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7305,6 +7305,12 @@ ZEND_METHOD(ReflectionAttribute, newInstance) RETURN_THROWS(); } + zend_attribute *delayed_target_validation = zend_get_attribute_str( + attr->attributes, + "delayedtargetvalidation", + strlen("delayedtargetvalidation") + ); + /* This code can be reached under one of three possible conditions: * - the attribute is an internal attribute, and it had the target and * and repetition validated already @@ -7313,17 +7319,15 @@ ZEND_METHOD(ReflectionAttribute, newInstance) * #[DelayedTargetValidation] * - the attribute is a user attribute, and neither target nor repetition * have been validated. - * - * It is not worth checking for the presence of #[DelayedTargetValidation] - * to determine if we should run target validation for internal attributes; - * it is faster just to do the validation, which will always pass if the - * attribute is absent. */ uint32_t flags = zend_attribute_attribute_get_flags(marker, ce); if (EG(exception)) { RETURN_THROWS(); } + /* No harm in always running target validation, for internal attributes + * with #[DelayedTargetValidation] it isn't necessary but will always + * succeed. */ if (!(attr->target & flags)) { zend_string *location = zend_get_attribute_target_names(attr->target); zend_string *allowed = zend_get_attribute_target_names(flags); @@ -7339,12 +7343,12 @@ ZEND_METHOD(ReflectionAttribute, newInstance) } /* Run the delayed validator function for internal attributes */ - if (ce->type == ZEND_INTERNAL_CLASS) { + if (delayed_target_validation && ce->type == ZEND_INTERNAL_CLASS) { zend_internal_attribute *config = zend_internal_attribute_get(attr->data->lcname); if (config != NULL && config->validator != NULL) { config->validator( attr->data, - attr->target | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION, + flags | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION, attr->scope ); if (EG(exception)) { From 7781f2c6f8fb41e63f0f0dbfa5e9540347f23fec Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 9 Jul 2025 16:07:59 -0700 Subject: [PATCH 5/9] Test application of all internal attributes, fix 2 bugs --- .../with_AllowDynamicProperties.phpt | 67 +++++++++++++++ .../with_Attribute.phpt | 81 +++++++++++++++++++ .../with_Deprecated.phpt | 66 +++++++++++++++ .../with_NoDiscard.phpt | 66 +++++++++++++++ .../with_Override_error.phpt | 18 +++++ .../with_Override_okay.phpt | 64 +++++++++++++++ .../with_ReturnTypeWillChange.phpt | 70 ++++++++++++++++ .../with_SensitiveParameter.phpt | 70 ++++++++++++++++ Zend/zend_attributes.c | 7 ++ Zend/zend_compile.c | 22 +++-- 10 files changed, 526 insertions(+), 5 deletions(-) create mode 100644 Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/with_Override_error.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt create mode 100644 Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt diff --git a/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt b/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt new file mode 100644 index 0000000000000..b9154f37f73d0 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt @@ -0,0 +1,67 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\AllowDynamicProperties]: invalid targets don't error +--FILE-- +val = $str; + } + + #[DelayedTargetValidation] + #[AllowDynamicProperties] // Does nothing here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + } + +} + +#[DelayedTargetValidation] +#[AllowDynamicProperties] // Does nothing here +function demoFn() { + echo __FUNCTION__ . "\n"; + return 456; +} + +#[DelayedTargetValidation] +#[AllowDynamicProperties] // Does nothing here +const GLOBAL_CONST = 'BAR'; + +$d = new DemoClass('example'); +$d->printVal(); +var_dump($d->val); +var_dump(DemoClass::CLASS_CONST); +demoFn(); +var_dump(GLOBAL_CONST); + +$d->missingProp = 'foo'; +var_dump($d); +?> +--EXPECTF-- +Got: example +Value is: example +string(7) "example" +string(3) "FOO" +demoFn +string(3) "BAR" +object(DemoClass)#%d (2) { + ["val"]=> + string(7) "example" + ["missingProp"]=> + string(3) "foo" +} diff --git a/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt b/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt new file mode 100644 index 0000000000000..13b33dca53ee8 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt @@ -0,0 +1,81 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Attribute]: invalid targets don't error +--FILE-- +val = $str; + } + + #[DelayedTargetValidation] + #[Attribute] // Does nothing here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + } + +} + +#[DelayedTargetValidation] +#[Attribute] // Does nothing here +function demoFn() { + echo __FUNCTION__ . "\n"; + return 456; +} + +#[DelayedTargetValidation] +#[Attribute] // Does nothing here +const GLOBAL_CONST = 'BAR'; + +$d = new DemoClass('example'); +$d->printVal(); +var_dump($d->val); +var_dump(DemoClass::CLASS_CONST); +demoFn(); +var_dump(GLOBAL_CONST); + +#[DemoClass('BAZ')] +#[NonAttribute] +class WithDemoAttribs {} + +$ref = new ReflectionClass(WithDemoAttribs::class); +$attribs = $ref->getAttributes(); +var_dump($attribs[0]->newInstance()); +var_dump($attribs[1]->newInstance()); + +?> +--EXPECTF-- +Got: example +Value is: example +string(7) "example" +string(3) "FOO" +demoFn +string(3) "BAR" +Got: BAZ +object(DemoClass)#5 (1) { + ["val"]=> + string(3) "BAZ" +} + +Fatal error: Uncaught Error: Attempting to use non-attribute class "NonAttribute" as attribute in %s:%d +Stack trace: +#0 %s(%d): ReflectionAttribute->newInstance() +#1 {main} + thrown in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt b/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt new file mode 100644 index 0000000000000..e0d3d4c4ae592 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt @@ -0,0 +1,66 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Deprecated]: valid targets are deprecated +--FILE-- +val = $str; + } + + #[DelayedTargetValidation] + #[Deprecated] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + return 123; + } +} + +#[DelayedTargetValidation] +#[Deprecated] // Does something here +function demoFn() { + echo __FUNCTION__ . "\n"; + return 456; +} + +#[DelayedTargetValidation] +#[Deprecated] // Does something here +const GLOBAL_CONST = 'BAR'; + +$d = new DemoClass('example'); +$d->printVal(); +var_dump($d->val); +var_dump(DemoClass::CLASS_CONST); +demoFn(); +var_dump(GLOBAL_CONST); +?> +--EXPECTF-- +Got: example + +Deprecated: Method DemoClass::printVal() is deprecated in %s on line %d +Value is: example +string(7) "example" + +Deprecated: Constant DemoClass::CLASS_CONST is deprecated in %s on line %d +string(3) "FOO" + +Deprecated: Function demoFn() is deprecated in %s on line %d +demoFn + +Deprecated: Constant GLOBAL_CONST is deprecated in %s on line %d +string(3) "BAR" diff --git a/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt new file mode 100644 index 0000000000000..affc3a691deaf --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt @@ -0,0 +1,66 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\NoDiscard]: valid targets complain about discarding +--FILE-- +val = $str; + } + + #[DelayedTargetValidation] + #[NoDiscard] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + return 123; + } +} + +#[DelayedTargetValidation] +#[NoDiscard] // Does something here +function demoFn() { + echo __FUNCTION__ . "\n"; + return 456; +} + +#[DelayedTargetValidation] +#[NoDiscard] // Does nothing here +const GLOBAL_CONST = 'BAR'; + +$d = new DemoClass('example'); +$d->printVal(); +$v = $d->printVal(); +var_dump($d->val); +var_dump(DemoClass::CLASS_CONST); +demoFn(); +$v = demoFn(); +var_dump(GLOBAL_CONST); +?> +--EXPECTF-- +Got: example + +Warning: The return value of method DemoClass::printVal() should either be used or intentionally ignored by casting it as (void) in %s on line %d +Value is: example +Value is: example +string(7) "example" +string(3) "FOO" + +Warning: The return value of function demoFn() should either be used or intentionally ignored by casting it as (void) in %s on line %d +demoFn +demoFn +string(3) "BAR" diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error.phpt new file mode 100644 index 0000000000000..46b05713917b0 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error +--FILE-- +val . "\n"; + return 123; + } +} + +?> +--EXPECTF-- +Fatal error: DemoClass::printVal() has #[\Override] attribute, but no matching parent method exists in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt new file mode 100644 index 0000000000000..33e171e1be156 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt @@ -0,0 +1,64 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: invalid targets or actual overrides don't do anything +--FILE-- +val = $str; + } + + #[DelayedTargetValidation] + #[Override] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + return 123; + } +} + +#[DelayedTargetValidation] +#[Override] // Does nothing here +function demoFn() { + echo __FUNCTION__ . "\n"; + return 456; +} + +#[DelayedTargetValidation] +#[Override] // Does nothing here +const GLOBAL_CONST = 'BAR'; + +$d = new DemoClass('example'); +$d->printVal(); +var_dump($d->val); +var_dump(DemoClass::CLASS_CONST); +demoFn(); +var_dump(GLOBAL_CONST); +?> +--EXPECT-- +Got: example +Value is: example +string(7) "example" +string(3) "FOO" +demoFn +string(3) "BAR" diff --git a/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt b/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt new file mode 100644 index 0000000000000..0cc3bb4a4b9a4 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt @@ -0,0 +1,70 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\ReturnTypeWillChange]: valid targets suppress return type warnings +--FILE-- +val = $str; + } + + #[DelayedTargetValidation] + #[ReturnTypeWillChange] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + } + + #[DelayedTargetValidation] + #[ReturnTypeWillChange] // Does something here + public function count() { + return 5; + } +} + +#[DelayedTargetValidation] +#[ReturnTypeWillChange] // Does nothing here +function demoFn() { + echo __FUNCTION__ . "\n"; + return 456; +} + +#[DelayedTargetValidation] +#[ReturnTypeWillChange] // Does nothing here +const GLOBAL_CONST = 'BAR'; + +$d = new DemoClass('example'); +$d->printVal(); +var_dump($d->val); +var_dump(DemoClass::CLASS_CONST); +demoFn(); +var_dump(GLOBAL_CONST); +?> +--EXPECTF-- +Deprecated: Return type of WithoutAttrib::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in %s on line %d +Got: example +Value is: example +string(7) "example" +string(3) "FOO" +demoFn +string(3) "BAR" diff --git a/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt b/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt new file mode 100644 index 0000000000000..440d47d42b38e --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt @@ -0,0 +1,70 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\SensitiveParameter]: parameter still redacted +--FILE-- +val = $str; + } + + #[DelayedTargetValidation] + #[SensitiveParameter] // Does nothing here + public function printVal( + #[DelayedTargetValidation] + #[SensitiveParameter] + $sensitive + ) { + throw new Exception('Testing backtrace'); + } + +} + +#[DelayedTargetValidation] +#[SensitiveParameter] // Does nothing here +function demoFn() { + echo __FUNCTION__ . "\n"; + return 456; +} + +#[DelayedTargetValidation] +#[SensitiveParameter] // Does nothing here +const GLOBAL_CONST = 'BAR'; + +$d = new DemoClass('example'); +var_dump($d->val); +var_dump(DemoClass::CLASS_CONST); +demoFn(); +var_dump(GLOBAL_CONST); + +$d->printVal('BAZ'); + + +?> +--EXPECTF-- +Got: example +string(7) "example" +string(3) "FOO" +demoFn +string(3) "BAR" + +Fatal error: Uncaught Exception: Testing backtrace in %s:%d +Stack trace: +#0 %s(%d): DemoClass->printVal(Object(SensitiveParameterValue)) +#1 {main} + thrown in %s on line %d diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index 5e7a9d3d6e71b..d24682c816602 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -73,6 +73,13 @@ uint32_t zend_attribute_attribute_get_flags(zend_attribute *attr, zend_class_ent static void validate_allow_dynamic_properties( zend_attribute *attr, uint32_t target, zend_class_entry *scope) { + if (scope == NULL) { + // Only reachable when validator is run but the attribute isn't applied + // to a class; in the case of delayed target validation reflection will + // complain about the target before running the validator; + ZEND_ASSERT(target & ZEND_ATTRIBUTE_NO_TARGET_VALIDATION); + return; + } const char *msg = NULL; if (scope->ce_flags & ZEND_ACC_TRAIT) { msg = "Cannot apply #[\\AllowDynamicProperties] to trait %s"; diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index af8ce4dc405da..5423d8a0aa89f 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -7544,11 +7544,23 @@ static void zend_compile_attributes( if (*attributes != NULL) { /* Allow delaying target validation for forward compatibility. */ - zend_attribute *delayed_target_validation = zend_get_attribute_str( - *attributes, - "delayedtargetvalidation", - strlen("delayedtargetvalidation") - ); + zend_attribute *delayed_target_validation = NULL; + if (target == ZEND_ATTRIBUTE_TARGET_PARAMETER) { + ZEND_ASSERT(offset >= 1); + // zend_get_parameter_attribute_str will add 1 too + delayed_target_validation = zend_get_parameter_attribute_str( + *attributes, + "delayedtargetvalidation", + strlen("delayedtargetvalidation"), + offset - 1 + ); + } else { + delayed_target_validation = zend_get_attribute_str( + *attributes, + "delayedtargetvalidation", + strlen("delayedtargetvalidation") + ); + } uint32_t extra_flags = delayed_target_validation ? ZEND_ATTRIBUTE_NO_TARGET_VALIDATION : 0; /* Validate attributes in a secondary loop (needed to detect repeated attributes). */ ZEND_HASH_PACKED_FOREACH_PTR(*attributes, attr) { From 08aa1100a7a695e00d8e96c612fb0ce58e7bb905 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 9 Jul 2025 16:12:56 -0700 Subject: [PATCH 6/9] Missing comment --- .../delayed_target_validation/with_SensitiveParameter.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt b/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt index 440d47d42b38e..fb0ffb737a562 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt @@ -28,7 +28,7 @@ class DemoClass { public function printVal( #[DelayedTargetValidation] #[SensitiveParameter] - $sensitive + $sensitive // Does something here ) { throw new Exception('Testing backtrace'); } From c3b46a38c472ca810156df1e4bfbc6bb4634e331 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 25 Jul 2025 10:47:21 -0700 Subject: [PATCH 7/9] Tests for property hooks --- .../errors_from_validator.phpt | 17 ++++ .../has_runtime_errors.phpt | 85 ++++++++++++++++--- .../no_compile_errors.phpt | 15 +++- .../with_AllowDynamicProperties.phpt | 58 ++++++++----- .../with_Attribute.phpt | 56 +++++++----- .../with_Deprecated.phpt | 60 ++++++++----- .../with_NoDiscard.phpt | 58 ++++++++----- .../with_Override_error_get.phpt | 18 ++++ ...r.phpt => with_Override_error_method.phpt} | 14 +-- .../with_Override_error_set.phpt | 18 ++++ .../with_Override_okay.phpt | 68 +++++++++------ .../with_ReturnTypeWillChange.phpt | 70 ++++++++------- .../with_SensitiveParameter.phpt | 62 ++++++++------ 13 files changed, 409 insertions(+), 190 deletions(-) create mode 100644 Zend/tests/attributes/delayed_target_validation/with_Override_error_get.phpt rename Zend/tests/attributes/delayed_target_validation/{with_Override_error.phpt => with_Override_error_method.phpt} (58%) create mode 100644 Zend/tests/attributes/delayed_target_validation/with_Override_error_set.phpt diff --git a/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt b/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt index d0948e5fd7ad0..f14bda38158d3 100644 --- a/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt +++ b/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt @@ -19,11 +19,28 @@ readonly class DemoReadonly {} #[AllowDynamicProperties] enum DemoEnum {} +class DemoClass { + #[DelayedTargetValidation] + #[NoDiscard] // Does nothing here + public $val; + + public string $hooked { + #[DelayedTargetValidation] + // #[NoDiscard] // Does nothing here + get => $this->hooked; + #[DelayedTargetValidation] + // #[NoDiscard] // Does nothing here + set => $value; + } +} + $cases = [ new ReflectionClass('DemoTrait'), new ReflectionClass('DemoInterface'), new ReflectionClass('DemoReadonly'), new ReflectionClass('DemoEnum'), + // new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Get), + // new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Set), ]; foreach ($cases as $r) { echo str_repeat("*", 20) . "\n"; diff --git a/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt index bb65b126172fb..7a2f9d32c0b2b 100644 --- a/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt +++ b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt @@ -13,16 +13,25 @@ class Demo { #[DelayedTargetValidation] #[Attribute] - public string $v; + public string $v1; + + public string $v2 { + #[DelayedTargetValidation] + #[Attribute] + get => $this->v2; + #[DelayedTargetValidation] + #[Attribute] + set => $value; + } #[DelayedTargetValidation] #[Attribute] public function __construct( #[DelayedTargetValidation] #[Attribute] - public string $v2 + public string $v3 ) { - $this->v = $v2; + $this->v1 = $v3; echo __METHOD__ . "\n"; } } @@ -40,10 +49,12 @@ const EXAMPLE = true; $cases = [ new ReflectionClass('Demo'), new ReflectionClassConstant('Demo', 'FOO'), - new ReflectionProperty('Demo', 'v'), + new ReflectionProperty('Demo', 'v1'), + new ReflectionProperty('Demo', 'v2')->getHook(PropertyHookType::Get), + new ReflectionProperty('Demo', 'v2')->getHook(PropertyHookType::Set), new ReflectionMethod('Demo', '__construct'), - new ReflectionParameter([ 'Demo', '__construct' ], 'v2'), - new ReflectionProperty('Demo', 'v2'), + new ReflectionParameter([ 'Demo', '__construct' ], 'v3'), + new ReflectionProperty('Demo', 'v3'), new ReflectionFunction('demoFn'), new ReflectionConstant('EXAMPLE'), ]; @@ -62,7 +73,7 @@ foreach ($cases as $r) { ?> --EXPECTF-- ******************** -Class [ class Demo ] { +Class [ class Demo ] { @@ %s %d-%d - Constants [1] { @@ -75,9 +86,10 @@ Class [ class Demo ] { - Static methods [0] { } - - Properties [2] { - Property [ public string $v ] + - Properties [3] { + Property [ public string $v1 ] Property [ public string $v2 ] + Property [ public string $v3 ] } - Methods [1] { @@ -85,7 +97,7 @@ Class [ class Demo ] { @@ %s %d - %d - Parameters [1] { - Parameter #0 [ string $v2 ] + Parameter #0 [ string $v3 ] } } } @@ -121,7 +133,7 @@ array(2) { } Error: Attribute "Attribute" cannot target class constant (allowed targets: class) ******************** -Property [ public string $v ] +Property [ public string $v1 ] array(2) { [0]=> @@ -137,11 +149,56 @@ array(2) { } Error: Attribute "Attribute" cannot target property (allowed targets: class) ******************** +Method [ public method $v2::get ] { + @@ %s %d - %d + + - Parameters [0] { + } + - Return [ string ] +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target method (allowed targets: class) +******************** +Method [ public method $v2::set ] { + @@ %s %d - %d + + - Parameters [1] { + Parameter #0 [ string $value ] + } + - Return [ void ] +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "Attribute" + } +} +Error: Attribute "Attribute" cannot target method (allowed targets: class) +******************** Method [ public method __construct ] { @@ %s %d - %d - Parameters [1] { - Parameter #0 [ string $v2 ] + Parameter #0 [ string $v3 ] } } @@ -159,7 +216,7 @@ array(2) { } Error: Attribute "Attribute" cannot target method (allowed targets: class) ******************** -Parameter #0 [ string $v2 ] +Parameter #0 [ string $v3 ] array(2) { [0]=> object(ReflectionAttribute)#%d (1) { @@ -174,7 +231,7 @@ array(2) { } Error: Attribute "Attribute" cannot target parameter (allowed targets: class) ******************** -Property [ public string $v2 ] +Property [ public string $v3 ] array(2) { [0]=> diff --git a/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt b/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt index ea96a069d748c..b2e14a235f8a5 100644 --- a/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt +++ b/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt @@ -13,16 +13,25 @@ class Demo { #[DelayedTargetValidation] #[Attribute] - public string $v; + public string $v1; + + public string $v2 { + #[DelayedTargetValidation] + #[Attribute] + get => $this->v2; + #[DelayedTargetValidation] + #[Attribute] + set => $value; + } #[DelayedTargetValidation] #[Attribute] public function __construct( #[DelayedTargetValidation] #[Attribute] - public string $v2 + public string $v3 ) { - $this->v = $v2; + $this->v1 = $v3; echo __METHOD__ . "\n"; } } diff --git a/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt b/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt index b9154f37f73d0..83f738491c57e 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt @@ -6,36 +6,45 @@ #[DelayedTargetValidation] #[AllowDynamicProperties] // Does something here class DemoClass { - #[DelayedTargetValidation] - #[AllowDynamicProperties] // Does nothing here - public $val; + #[DelayedTargetValidation] + #[AllowDynamicProperties] // Does nothing here + public $val; - #[DelayedTargetValidation] - #[AllowDynamicProperties] // Does nothing here - public const CLASS_CONST = 'FOO'; + public string $hooked { + #[DelayedTargetValidation] + #[AllowDynamicProperties] // Does nothing here + get => $this->hooked; + #[DelayedTargetValidation] + #[AllowDynamicProperties] // Does nothing here + set => $value; + } - public function __construct( - #[DelayedTargetValidation] - #[AllowDynamicProperties] // Does nothing here - $str - ) { - echo "Got: $str\n"; - $this->val = $str; - } + #[DelayedTargetValidation] + #[AllowDynamicProperties] // Does nothing here + public const CLASS_CONST = 'FOO'; - #[DelayedTargetValidation] - #[AllowDynamicProperties] // Does nothing here - public function printVal() { - echo 'Value is: ' . $this->val . "\n"; - } + public function __construct( + #[DelayedTargetValidation] + #[AllowDynamicProperties] // Does nothing here + $str + ) { + echo "Got: $str\n"; + $this->val = $str; + } + + #[DelayedTargetValidation] + #[AllowDynamicProperties] // Does nothing here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + } } #[DelayedTargetValidation] #[AllowDynamicProperties] // Does nothing here function demoFn() { - echo __FUNCTION__ . "\n"; - return 456; + echo __FUNCTION__ . "\n"; + return 456; } #[DelayedTargetValidation] @@ -45,6 +54,8 @@ const GLOBAL_CONST = 'BAR'; $d = new DemoClass('example'); $d->printVal(); var_dump($d->val); +$d->hooked = "foo"; +var_dump($d->hooked); var_dump(DemoClass::CLASS_CONST); demoFn(); var_dump(GLOBAL_CONST); @@ -56,12 +67,15 @@ var_dump($d); Got: example Value is: example string(7) "example" +string(3) "foo" string(3) "FOO" demoFn string(3) "BAR" -object(DemoClass)#%d (2) { +object(DemoClass)#%d (3) { ["val"]=> string(7) "example" + ["hooked"]=> + string(3) "foo" ["missingProp"]=> string(3) "foo" } diff --git a/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt b/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt index 13b33dca53ee8..edc7d2a7905fb 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt @@ -8,36 +8,45 @@ class NonAttribute {} #[DelayedTargetValidation] #[Attribute] // Does something here class DemoClass { - #[DelayedTargetValidation] - #[Attribute] // Does nothing here - public $val; + #[DelayedTargetValidation] + #[Attribute] // Does nothing here + public $val; + + public string $hooked { + #[DelayedTargetValidation] + #[Attribute] // Does nothing here + get => $this->hooked; + #[DelayedTargetValidation] + #[Attribute] // Does nothing here + set => $value; + } - #[DelayedTargetValidation] - #[Attribute] // Does nothing here - public const CLASS_CONST = 'FOO'; + #[DelayedTargetValidation] + #[Attribute] // Does nothing here + public const CLASS_CONST = 'FOO'; - public function __construct( - #[DelayedTargetValidation] - #[Attribute] // Does nothing here - $str - ) { - echo "Got: $str\n"; - $this->val = $str; - } + public function __construct( + #[DelayedTargetValidation] + #[Attribute] // Does nothing here + $str + ) { + echo "Got: $str\n"; + $this->val = $str; + } - #[DelayedTargetValidation] - #[Attribute] // Does nothing here - public function printVal() { - echo 'Value is: ' . $this->val . "\n"; - } + #[DelayedTargetValidation] + #[Attribute] // Does nothing here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + } } #[DelayedTargetValidation] #[Attribute] // Does nothing here function demoFn() { - echo __FUNCTION__ . "\n"; - return 456; + echo __FUNCTION__ . "\n"; + return 456; } #[DelayedTargetValidation] @@ -47,6 +56,8 @@ const GLOBAL_CONST = 'BAR'; $d = new DemoClass('example'); $d->printVal(); var_dump($d->val); +$d->hooked = "foo"; +var_dump($d->hooked); var_dump(DemoClass::CLASS_CONST); demoFn(); var_dump(GLOBAL_CONST); @@ -65,6 +76,7 @@ var_dump($attribs[1]->newInstance()); Got: example Value is: example string(7) "example" +string(3) "foo" string(3) "FOO" demoFn string(3) "BAR" @@ -72,6 +84,8 @@ Got: BAZ object(DemoClass)#5 (1) { ["val"]=> string(3) "BAZ" + ["hooked"]=> + uninitialized(string) } Fatal error: Uncaught Error: Attempting to use non-attribute class "NonAttribute" as attribute in %s:%d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt b/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt index e0d3d4c4ae592..093b0abb08e0c 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt @@ -6,36 +6,45 @@ #[DelayedTargetValidation] #[Deprecated] // Does nothing here class DemoClass { - #[DelayedTargetValidation] - #[Deprecated] // Does nothing here - public $val; + #[DelayedTargetValidation] + #[Deprecated] // Does nothing here + public $val; - #[DelayedTargetValidation] - #[Deprecated] // Does something here - public const CLASS_CONST = 'FOO'; + public string $hooked { + #[DelayedTargetValidation] + #[Deprecated] // Does something here + get => $this->hooked; + #[DelayedTargetValidation] + #[Deprecated] // Does something here + set => $value; + } - public function __construct( - #[DelayedTargetValidation] - #[Deprecated] // Does nothing here - $str - ) { - echo "Got: $str\n"; - $this->val = $str; - } + #[DelayedTargetValidation] + #[Deprecated] // Does something here + public const CLASS_CONST = 'FOO'; - #[DelayedTargetValidation] - #[Deprecated] // Does something here - public function printVal() { - echo 'Value is: ' . $this->val . "\n"; - return 123; - } + public function __construct( + #[DelayedTargetValidation] + #[Deprecated] // Does nothing here + $str + ) { + echo "Got: $str\n"; + $this->val = $str; + } + + #[DelayedTargetValidation] + #[Deprecated] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + return 123; + } } #[DelayedTargetValidation] #[Deprecated] // Does something here function demoFn() { - echo __FUNCTION__ . "\n"; - return 456; + echo __FUNCTION__ . "\n"; + return 456; } #[DelayedTargetValidation] @@ -45,6 +54,8 @@ const GLOBAL_CONST = 'BAR'; $d = new DemoClass('example'); $d->printVal(); var_dump($d->val); +$d->hooked = "foo"; +var_dump($d->hooked); var_dump(DemoClass::CLASS_CONST); demoFn(); var_dump(GLOBAL_CONST); @@ -56,6 +67,11 @@ Deprecated: Method DemoClass::printVal() is deprecated in %s on line %d Value is: example string(7) "example" +Deprecated: Method DemoClass::$hooked::set() is deprecated in %s on line %d + +Deprecated: Method DemoClass::$hooked::get() is deprecated in %s on line %d +string(3) "foo" + Deprecated: Constant DemoClass::CLASS_CONST is deprecated in %s on line %d string(3) "FOO" diff --git a/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt index affc3a691deaf..0ffe5cf78763e 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt @@ -6,36 +6,45 @@ #[DelayedTargetValidation] #[NoDiscard] // Does nothing here class DemoClass { - #[DelayedTargetValidation] - #[NoDiscard] // Does nothing here - public $val; + #[DelayedTargetValidation] + #[NoDiscard] // Does nothing here + public $val; - #[DelayedTargetValidation] - #[NoDiscard] // Does nothing here - public const CLASS_CONST = 'FOO'; + public string $hooked { + #[DelayedTargetValidation] + // #[NoDiscard] // Does nothing here + get => $this->hooked; + #[DelayedTargetValidation] + // #[NoDiscard] // Does nothing here + set => $value; + } - public function __construct( - #[DelayedTargetValidation] - #[NoDiscard] // Does nothing here - $str - ) { - echo "Got: $str\n"; - $this->val = $str; - } + #[DelayedTargetValidation] + #[NoDiscard] // Does nothing here + public const CLASS_CONST = 'FOO'; - #[DelayedTargetValidation] - #[NoDiscard] // Does something here - public function printVal() { - echo 'Value is: ' . $this->val . "\n"; - return 123; - } + public function __construct( + #[DelayedTargetValidation] + #[NoDiscard] // Does nothing here + $str + ) { + echo "Got: $str\n"; + $this->val = $str; + } + + #[DelayedTargetValidation] + #[NoDiscard] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + return 123; + } } #[DelayedTargetValidation] #[NoDiscard] // Does something here function demoFn() { - echo __FUNCTION__ . "\n"; - return 456; + echo __FUNCTION__ . "\n"; + return 456; } #[DelayedTargetValidation] @@ -46,6 +55,10 @@ $d = new DemoClass('example'); $d->printVal(); $v = $d->printVal(); var_dump($d->val); +$d->hooked = "foo"; +var_dump($d->hooked); +// NODiscard does not support property hooks, this should not complain +$d->hooked; var_dump(DemoClass::CLASS_CONST); demoFn(); $v = demoFn(); @@ -58,6 +71,7 @@ Warning: The return value of method DemoClass::printVal() should either be used Value is: example Value is: example string(7) "example" +string(3) "foo" string(3) "FOO" Warning: The return value of function demoFn() should either be used or intentionally ignored by casting it as (void) in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_get.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_get.phpt new file mode 100644 index 0000000000000..a33e83d517a30 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_get.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (get hook) +--FILE-- + $this->hooked; + set => $value; + } +} + +?> +--EXPECTF-- +Fatal error: DemoClass::$hooked::get() has #[\Override] attribute, but no matching parent method exists in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_method.phpt similarity index 58% rename from Zend/tests/attributes/delayed_target_validation/with_Override_error.phpt rename to Zend/tests/attributes/delayed_target_validation/with_Override_error_method.phpt index 46b05713917b0..ecca1daff0fd3 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_Override_error.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_method.phpt @@ -1,16 +1,16 @@ --TEST-- -#[\DelayedTargetValidation] with #[\Override]: non-overrides still error +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (method) --FILE-- val . "\n"; - return 123; - } + #[DelayedTargetValidation] + #[Override] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + return 123; + } } ?> diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_set.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_set.phpt new file mode 100644 index 0000000000000..d99580646c548 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_set.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (set hook) +--FILE-- + $this->hooked; + #[DelayedTargetValidation] + #[Override] // Does something here + set => $value; + } +} + +?> +--EXPECTF-- +Fatal error: DemoClass::$hooked::set() has #[\Override] attribute, but no matching parent method exists in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt index 33e171e1be156..505a4ddb9e003 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt @@ -4,44 +4,59 @@ $this->hooked; + set => $value; + } + + public function printVal() { + echo __METHOD__ . "\n"; + } } #[DelayedTargetValidation] #[Override] // Does nothing here class DemoClass extends Base { - #[DelayedTargetValidation] - #[Override] // Does nothing here - public $val; + #[DelayedTargetValidation] + #[Override] // Does nothing here + public $val; + + public string $hooked { + #[DelayedTargetValidation] + #[Override] // Does something here + get => $this->hooked; + #[DelayedTargetValidation] + #[Override] // Does something here + set => $value; + } - #[DelayedTargetValidation] - #[Override] // Does nothing here - public const CLASS_CONST = 'FOO'; + #[DelayedTargetValidation] + #[Override] // Does nothing here + public const CLASS_CONST = 'FOO'; - public function __construct( - #[DelayedTargetValidation] - #[Override] // Does nothing here - $str - ) { - echo "Got: $str\n"; - $this->val = $str; - } + public function __construct( + #[DelayedTargetValidation] + #[Override] // Does nothing here + $str + ) { + echo "Got: $str\n"; + $this->val = $str; + } - #[DelayedTargetValidation] - #[Override] // Does something here - public function printVal() { - echo 'Value is: ' . $this->val . "\n"; - return 123; - } + #[DelayedTargetValidation] + #[Override] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + return 123; + } } #[DelayedTargetValidation] #[Override] // Does nothing here function demoFn() { - echo __FUNCTION__ . "\n"; - return 456; + echo __FUNCTION__ . "\n"; + return 456; } #[DelayedTargetValidation] @@ -51,6 +66,8 @@ const GLOBAL_CONST = 'BAR'; $d = new DemoClass('example'); $d->printVal(); var_dump($d->val); +$d->hooked = "foo"; +var_dump($d->hooked); var_dump(DemoClass::CLASS_CONST); demoFn(); var_dump(GLOBAL_CONST); @@ -59,6 +76,7 @@ var_dump(GLOBAL_CONST); Got: example Value is: example string(7) "example" +string(3) "foo" string(3) "FOO" demoFn string(3) "BAR" diff --git a/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt b/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt index 0cc3bb4a4b9a4..c562075cd6c05 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt @@ -4,49 +4,58 @@ $this->hooked; + #[DelayedTargetValidation] + #[ReturnTypeWillChange] // Does nothing here + set => $value; + } - public function __construct( - #[DelayedTargetValidation] - #[ReturnTypeWillChange] // Does nothing here - $str - ) { - echo "Got: $str\n"; - $this->val = $str; - } + #[DelayedTargetValidation] + #[ReturnTypeWillChange] // Does nothing here + public const CLASS_CONST = 'FOO'; - #[DelayedTargetValidation] - #[ReturnTypeWillChange] // Does something here - public function printVal() { - echo 'Value is: ' . $this->val . "\n"; - } + public function __construct( + #[DelayedTargetValidation] + #[ReturnTypeWillChange] // Does nothing here + $str + ) { + echo "Got: $str\n"; + $this->val = $str; + } - #[DelayedTargetValidation] - #[ReturnTypeWillChange] // Does something here - public function count() { - return 5; - } + #[DelayedTargetValidation] + #[ReturnTypeWillChange] // Does something here + public function printVal() { + echo 'Value is: ' . $this->val . "\n"; + } + + #[DelayedTargetValidation] + #[ReturnTypeWillChange] // Does something here + public function count() { + return 5; + } } #[DelayedTargetValidation] #[ReturnTypeWillChange] // Does nothing here function demoFn() { - echo __FUNCTION__ . "\n"; - return 456; + echo __FUNCTION__ . "\n"; + return 456; } #[DelayedTargetValidation] @@ -56,6 +65,8 @@ const GLOBAL_CONST = 'BAR'; $d = new DemoClass('example'); $d->printVal(); var_dump($d->val); +$d->hooked = "foo"; +var_dump($d->hooked); var_dump(DemoClass::CLASS_CONST); demoFn(); var_dump(GLOBAL_CONST); @@ -65,6 +76,7 @@ Deprecated: Return type of WithoutAttrib::count() should either be compatible wi Got: example Value is: example string(7) "example" +string(3) "foo" string(3) "FOO" demoFn string(3) "BAR" diff --git a/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt b/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt index fb0ffb737a562..270c031f6bbf0 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt @@ -6,40 +6,49 @@ #[DelayedTargetValidation] #[SensitiveParameter] // Does nothing here class DemoClass { - #[DelayedTargetValidation] - #[SensitiveParameter] // Does nothing here - public $val; + #[DelayedTargetValidation] + #[SensitiveParameter] // Does nothing here + public $val; - #[DelayedTargetValidation] - #[SensitiveParameter] // Does nothing here - public const CLASS_CONST = 'FOO'; + public string $hooked { + #[DelayedTargetValidation] + #[SensitiveParameter] // Does nothing here + get => $this->hooked; + #[DelayedTargetValidation] + #[SensitiveParameter] // Does nothing here + set => $value; + } - public function __construct( - #[DelayedTargetValidation] - #[SensitiveParameter] // Does something here - $str - ) { - echo "Got: $str\n"; - $this->val = $str; - } + #[DelayedTargetValidation] + #[SensitiveParameter] // Does nothing here + public const CLASS_CONST = 'FOO'; - #[DelayedTargetValidation] - #[SensitiveParameter] // Does nothing here - public function printVal( - #[DelayedTargetValidation] - #[SensitiveParameter] - $sensitive // Does something here - ) { - throw new Exception('Testing backtrace'); - } + public function __construct( + #[DelayedTargetValidation] + #[SensitiveParameter] // Does something here + $str + ) { + echo "Got: $str\n"; + $this->val = $str; + } + + #[DelayedTargetValidation] + #[SensitiveParameter] // Does nothing here + public function printVal( + #[DelayedTargetValidation] + #[SensitiveParameter] + $sensitive // Does something here + ) { + throw new Exception('Testing backtrace'); + } } #[DelayedTargetValidation] #[SensitiveParameter] // Does nothing here function demoFn() { - echo __FUNCTION__ . "\n"; - return 456; + echo __FUNCTION__ . "\n"; + return 456; } #[DelayedTargetValidation] @@ -48,6 +57,8 @@ const GLOBAL_CONST = 'BAR'; $d = new DemoClass('example'); var_dump($d->val); +$d->hooked = "foo"; +var_dump($d->hooked); var_dump(DemoClass::CLASS_CONST); demoFn(); var_dump(GLOBAL_CONST); @@ -59,6 +70,7 @@ $d->printVal('BAZ'); --EXPECTF-- Got: example string(7) "example" +string(3) "foo" string(3) "FOO" demoFn string(3) "BAR" From f87967f4c25b9cc0f66c7f6aba032fdbfdb9d0a3 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 25 Jul 2025 11:13:05 -0700 Subject: [PATCH 8/9] Delay validation of #[\NoDiscard] on property hooks --- .../errors_from_validator.phpt | 53 +++++++++++++++++-- .../with_NoDiscard.phpt | 4 +- Zend/zend_attributes.c | 26 +++++++++ Zend/zend_compile.c | 35 ++++++------ ext/reflection/php_reflection.c | 47 +++++++++++----- 5 files changed, 127 insertions(+), 38 deletions(-) diff --git a/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt b/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt index f14bda38158d3..9759f29e3336e 100644 --- a/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt +++ b/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt @@ -26,10 +26,10 @@ class DemoClass { public string $hooked { #[DelayedTargetValidation] - // #[NoDiscard] // Does nothing here + #[NoDiscard] // Does nothing here get => $this->hooked; #[DelayedTargetValidation] - // #[NoDiscard] // Does nothing here + #[NoDiscard] // Does nothing here set => $value; } } @@ -39,8 +39,8 @@ $cases = [ new ReflectionClass('DemoInterface'), new ReflectionClass('DemoReadonly'), new ReflectionClass('DemoEnum'), - // new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Get), - // new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Set), + new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Get), + new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Set), ]; foreach ($cases as $r) { echo str_repeat("*", 20) . "\n"; @@ -195,3 +195,48 @@ array(2) { } } Error: Cannot apply #[AllowDynamicProperties] to enum DemoEnum +******************** +Method [ public method $hooked::get ] { + @@ %s %d - %d + + - Parameters [0] { + } + - Return [ string ] +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "NoDiscard" + } +} +Error: #[\NoDiscard] is not supported for property hooks +******************** +Method [ public method $hooked::set ] { + @@ %s %d - %d + + - Parameters [1] { + Parameter #0 [ string $value ] + } + - Return [ void ] +} + +array(2) { + [0]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(23) "DelayedTargetValidation" + } + [1]=> + object(ReflectionAttribute)#%d (1) { + ["name"]=> + string(9) "NoDiscard" + } +} +Error: #[\NoDiscard] is not supported for property hooks diff --git a/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt index 0ffe5cf78763e..17baca1800512 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt @@ -12,10 +12,10 @@ class DemoClass { public string $hooked { #[DelayedTargetValidation] - // #[NoDiscard] // Does nothing here + #[NoDiscard] // Does nothing here get => $this->hooked; #[DelayedTargetValidation] - // #[NoDiscard] // Does nothing here + #[NoDiscard] // Does nothing here set => $value; } diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index d24682c816602..bb6c45daa22dc 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -210,6 +210,31 @@ ZEND_METHOD(Deprecated, __construct) } } +static void validate_nodiscard( + zend_attribute *attr, uint32_t target, zend_class_entry *scope) +{ + /* There isn't an easy way to identify the *method* that the attribute is + * applied to in a manner that works during both compilation (normal + * validation) and runtime (delayed validation). So, handle them separately. + */ + if (CG(in_compilation)) { + ZEND_ASSERT((target & ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION) == 0); + zend_op_array *op_array = CG(active_op_array); + const zend_string *prop_info_name = CG(context).active_property_info_name; + if (prop_info_name == NULL) { + op_array->fn_flags |= ZEND_ACC_NODISCARD; + return; + } + // Applied to a hook, either throw or ignore + if (target & ZEND_ATTRIBUTE_NO_TARGET_VALIDATION) { + return; + } + zend_error_noreturn(E_COMPILE_ERROR, "#[\\NoDiscard] is not supported for property hooks"); + } + /* At runtime, no way to identify the target method; Reflection will handle + * throwing the error if needed. */ +} + ZEND_METHOD(NoDiscard, __construct) { zend_string *message = NULL; @@ -569,6 +594,7 @@ void zend_register_attribute_ce(void) zend_ce_nodiscard = register_class_NoDiscard(); attr = zend_mark_internal_attribute(zend_ce_nodiscard); + attr->validator = validate_nodiscard; zend_ce_delayed_target_validation = register_class_DelayedTargetValidation(); attr = zend_mark_internal_attribute(zend_ce_delayed_target_validation); diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 5423d8a0aa89f..4b1ca92e4b39c 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -7568,8 +7568,9 @@ static void zend_compile_attributes( continue; } - if (delayed_target_validation == NULL) { - if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) { + bool run_validator = true; + if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) { + if (delayed_target_validation == NULL) { zend_string *location = zend_get_attribute_target_names(target); zend_string *allowed = zend_get_attribute_target_names(config->flags); @@ -7577,6 +7578,7 @@ static void zend_compile_attributes( ZSTR_VAL(attr->name), ZSTR_VAL(location), ZSTR_VAL(allowed) ); } + run_validator = false; } if (!(config->flags & ZEND_ATTRIBUTE_IS_REPEATABLE)) { @@ -7585,7 +7587,8 @@ static void zend_compile_attributes( } } - if (config->validator != NULL) { + // Validators are not run if the target is already invalid + if (run_validator && config->validator != NULL) { config->validator(attr, target | extra_flags, CG(active_class_entry)); } } ZEND_HASH_FOREACH_END(); @@ -8427,6 +8430,10 @@ static zend_op_array *zend_compile_func_decl_ex( CG(active_op_array) = op_array; + zend_oparray_context_begin(&orig_oparray_context, op_array); + CG(context).active_property_info_name = property_info_name; + CG(context).active_property_hook_kind = hook_kind; + if (decl->child[4]) { int target = ZEND_ATTRIBUTE_TARGET_FUNCTION; @@ -8456,15 +8463,7 @@ static zend_op_array *zend_compile_func_decl_ex( 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; - } + // ZEND_ACC_NODISCARD is added via an attribute validator } /* Do not leak the class scope into free standing functions, even if they are dynamically @@ -8478,10 +8477,6 @@ static zend_op_array *zend_compile_func_decl_ex( op_array->fn_flags |= ZEND_ACC_TOP_LEVEL; } - zend_oparray_context_begin(&orig_oparray_context, op_array); - CG(context).active_property_info_name = property_info_name; - CG(context).active_property_hook_kind = hook_kind; - { /* Push a separator to the loop variable stack */ zend_loop_var dummy_var; @@ -8516,9 +8511,11 @@ 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"); - } + /* ZEND_ACC_NODISCARD gets added by the attribute validator, but only + * if the method is not a hook; if it is a hook, then the validator + * should have either thrown an error or done nothing due to delayed + * target validation. */ + ZEND_ASSERT(!is_hook); if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) { zend_arg_info *return_info = CG(active_op_array)->arg_info - 1; diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 300f9fc3b59d2..d43eca46943a8 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -157,6 +157,7 @@ typedef struct _attribute_reference { zend_class_entry *scope; zend_string *filename; uint32_t target; + bool on_property_hook; } attribute_reference; typedef enum { @@ -1254,7 +1255,7 @@ static void _extension_string(smart_str *str, const zend_module_entry *module, c /* {{{ reflection_attribute_factory */ static void reflection_attribute_factory(zval *object, HashTable *attributes, zend_attribute *data, - zend_class_entry *scope, uint32_t target, zend_string *filename) + zend_class_entry *scope, uint32_t target, zend_string *filename, bool on_property_hook) { reflection_object *intern; attribute_reference *reference; @@ -1267,6 +1268,7 @@ static void reflection_attribute_factory(zval *object, HashTable *attributes, ze reference->scope = scope; reference->filename = filename ? zend_string_copy(filename) : NULL; reference->target = target; + reference->on_property_hook = on_property_hook; intern->ptr = reference; intern->ref_type = REF_TYPE_ATTRIBUTE; ZVAL_STR_COPY(reflection_prop_name(object), data->name); @@ -1274,7 +1276,8 @@ static void reflection_attribute_factory(zval *object, HashTable *attributes, ze /* }}} */ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *scope, - uint32_t offset, uint32_t target, zend_string *name, zend_class_entry *base, zend_string *filename) /* {{{ */ + uint32_t offset, uint32_t target, zend_string *name, zend_class_entry *base, zend_string *filename, + bool on_property_hook) /* {{{ */ { ZEND_ASSERT(attributes != NULL); @@ -1287,7 +1290,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s ZEND_HASH_PACKED_FOREACH_PTR(attributes, attr) { if (attr->offset == offset && zend_string_equals(attr->lcname, filter)) { - reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename); + reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename, on_property_hook); add_next_index_zval(ret, &tmp); } } ZEND_HASH_FOREACH_END(); @@ -1319,7 +1322,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s } } - reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename); + reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename, on_property_hook); add_next_index_zval(ret, &tmp); } ZEND_HASH_FOREACH_END(); @@ -1328,7 +1331,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s /* }}} */ static void reflect_attributes(INTERNAL_FUNCTION_PARAMETERS, HashTable *attributes, - uint32_t offset, zend_class_entry *scope, uint32_t target, zend_string *filename) /* {{{ */ + uint32_t offset, zend_class_entry *scope, uint32_t target, zend_string *filename, bool on_property_hook) /* {{{ */ { zend_string *name = NULL; zend_long flags = 0; @@ -1361,7 +1364,7 @@ static void reflect_attributes(INTERNAL_FUNCTION_PARAMETERS, HashTable *attribut array_init(return_value); - if (FAILURE == read_attributes(return_value, attributes, scope, offset, target, name, base, filename)) { + if (FAILURE == read_attributes(return_value, attributes, scope, offset, target, name, base, filename, on_property_hook)) { RETURN_THROWS(); } } @@ -2066,15 +2069,21 @@ ZEND_METHOD(ReflectionFunctionAbstract, getAttributes) GET_REFLECTION_OBJECT_PTR(fptr); + bool on_property_hook = false; + if (fptr->common.scope && (fptr->common.fn_flags & (ZEND_ACC_CLOSURE|ZEND_ACC_FAKE_CLOSURE)) != ZEND_ACC_CLOSURE) { target = ZEND_ATTRIBUTE_TARGET_METHOD; + if (fptr->common.prop_info != NULL) { + on_property_hook = true; + } } else { target = ZEND_ATTRIBUTE_TARGET_FUNCTION; } reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU, fptr->common.attributes, 0, fptr->common.scope, target, - fptr->type == ZEND_USER_FUNCTION ? fptr->op_array.filename : NULL); + fptr->type == ZEND_USER_FUNCTION ? fptr->op_array.filename : NULL, + on_property_hook); } /* }}} */ @@ -2916,7 +2925,8 @@ ZEND_METHOD(ReflectionParameter, getAttributes) reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU, attributes, param->offset + 1, scope, ZEND_ATTRIBUTE_TARGET_PARAMETER, - param->fptr->type == ZEND_USER_FUNCTION ? param->fptr->op_array.filename : NULL); + param->fptr->type == ZEND_USER_FUNCTION ? param->fptr->op_array.filename : NULL, + false); } /* {{{ Returns the index of the parameter, starting from 0 */ @@ -4038,7 +4048,8 @@ ZEND_METHOD(ReflectionClassConstant, getAttributes) reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU, ref->attributes, 0, ref->ce, ZEND_ATTRIBUTE_TARGET_CLASS_CONST, - ref->ce->type == ZEND_USER_CLASS ? ref->ce->info.user.filename : NULL); + ref->ce->type == ZEND_USER_CLASS ? ref->ce->info.user.filename : NULL, + false); } /* }}} */ @@ -4443,7 +4454,8 @@ ZEND_METHOD(ReflectionClass, getAttributes) reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU, ce->attributes, 0, ce, ZEND_ATTRIBUTE_TARGET_CLASS, - ce->type == ZEND_USER_CLASS ? ce->info.user.filename : NULL); + ce->type == ZEND_USER_CLASS ? ce->info.user.filename : NULL, + false); } /* }}} */ @@ -6385,7 +6397,8 @@ ZEND_METHOD(ReflectionProperty, getAttributes) reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU, ref->prop->attributes, 0, ref->prop->ce, ZEND_ATTRIBUTE_TARGET_PROPERTY, - ref->prop->ce->type == ZEND_USER_CLASS ? ref->prop->ce->info.user.filename : NULL); + ref->prop->ce->type == ZEND_USER_CLASS ? ref->prop->ce->info.user.filename : NULL, + false); } /* }}} */ @@ -7348,13 +7361,21 @@ ZEND_METHOD(ReflectionAttribute, newInstance) if (config != NULL && config->validator != NULL) { config->validator( attr->data, - flags | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION, + attr->target | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION, attr->scope ); if (EG(exception)) { RETURN_THROWS(); } } + /* For #[NoDiscard], the attribute does not work on property hooks, but + * at runtime the validator has no way to access the method that an + * attribute is applied to, attr->scope is just the overall class entry + * that the method is a part of. */ + if (ce == zend_ce_nodiscard && attr->on_property_hook) { + zend_throw_error(NULL, "#[\\NoDiscard] is not supported for property hooks");; + RETURN_THROWS(); + } } /* Repetition validation is done even if #[DelayedTargetValidation] is used @@ -7886,7 +7907,7 @@ ZEND_METHOD(ReflectionConstant, getAttributes) reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU, const_->attributes, 0, NULL, ZEND_ATTRIBUTE_TARGET_CONST, - const_->filename); + const_->filename, false); } ZEND_METHOD(ReflectionConstant, __toString) From 82f556adcbde647e4b600172b4d26d9aaf5a1c63 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 1 Aug 2025 07:37:54 -0700 Subject: [PATCH 9/9] Update tests; tweak comments --- .../errors_from_validator.phpt | 12 ++++-------- .../has_runtime_errors.phpt | 2 +- .../delayed_target_validation/with_NoDiscard.phpt | 2 +- Zend/zend_attributes.c | 9 ++++----- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt b/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt index 9759f29e3336e..1da43ea89c8c3 100644 --- a/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt +++ b/Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt @@ -20,10 +20,6 @@ readonly class DemoReadonly {} enum DemoEnum {} class DemoClass { - #[DelayedTargetValidation] - #[NoDiscard] // Does nothing here - public $val; - public string $hooked { #[DelayedTargetValidation] #[NoDiscard] // Does nothing here @@ -88,7 +84,7 @@ array(2) { string(22) "AllowDynamicProperties" } } -Error: Cannot apply #[AllowDynamicProperties] to trait DemoTrait +Error: Cannot apply #[\AllowDynamicProperties] to trait DemoTrait ******************** Interface [ interface DemoInterface ] { @@ %s %d-%d @@ -121,7 +117,7 @@ array(2) { string(22) "AllowDynamicProperties" } } -Error: Cannot apply #[AllowDynamicProperties] to interface DemoInterface +Error: Cannot apply #[\AllowDynamicProperties] to interface DemoInterface ******************** Class [ readonly class DemoReadonly ] { @@ %s %d-%d @@ -154,7 +150,7 @@ array(2) { string(22) "AllowDynamicProperties" } } -Error: Cannot apply #[AllowDynamicProperties] to readonly class DemoReadonly +Error: Cannot apply #[\AllowDynamicProperties] to readonly class DemoReadonly ******************** Enum [ enum DemoEnum implements UnitEnum ] { @@ %s %d-%d @@ -194,7 +190,7 @@ array(2) { string(22) "AllowDynamicProperties" } } -Error: Cannot apply #[AllowDynamicProperties] to enum DemoEnum +Error: Cannot apply #[\AllowDynamicProperties] to enum DemoEnum ******************** Method [ public method $hooked::get ] { @@ %s %d - %d diff --git a/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt index 7a2f9d32c0b2b..b1efb538b328b 100644 --- a/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt +++ b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt @@ -88,7 +88,7 @@ Class [ class Demo ] { - Properties [3] { Property [ public string $v1 ] - Property [ public string $v2 ] + Property [ public string $v2 { get; set; } ] Property [ public string $v3 ] } diff --git a/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt index 17baca1800512..f2571ec07c2f1 100644 --- a/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt +++ b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt @@ -57,7 +57,7 @@ $v = $d->printVal(); var_dump($d->val); $d->hooked = "foo"; var_dump($d->hooked); -// NODiscard does not support property hooks, this should not complain +// NoDiscard does not support property hooks, this should not complain $d->hooked; var_dump(DemoClass::CLASS_CONST); demoFn(); diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index bb6c45daa22dc..bd652c613ddc5 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -74,9 +74,9 @@ static void validate_allow_dynamic_properties( zend_attribute *attr, uint32_t target, zend_class_entry *scope) { if (scope == NULL) { - // Only reachable when validator is run but the attribute isn't applied - // to a class; in the case of delayed target validation reflection will - // complain about the target before running the validator; + /* Only reachable when validator is run but the attribute isn't applied + * to a class; in the case of delayed target validation reflection will + * complain about the target before running the validator. */ ZEND_ASSERT(target & ZEND_ATTRIBUTE_NO_TARGET_VALIDATION); return; } @@ -215,8 +215,7 @@ static void validate_nodiscard( { /* There isn't an easy way to identify the *method* that the attribute is * applied to in a manner that works during both compilation (normal - * validation) and runtime (delayed validation). So, handle them separately. - */ + * validation) and runtime (delayed validation). So, handle them separately. */ if (CG(in_compilation)) { ZEND_ASSERT((target & ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION) == 0); zend_op_array *op_array = CG(active_op_array);