From bcfc2d4629b73b8ae784d3554f199eeb9301c79c Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 9 Jun 2025 13:24:57 -0700 Subject: [PATCH 1/8] 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 c3633801be83e..11858bd68439b 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 14afe40c01adf..57631fd82bb3d 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") @@ -290,3 +290,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 f3f6d1b75aec1..8c257c14fd86e 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -7518,19 +7518,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)) { @@ -7540,7 +7549,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 78817152904fb..6114ea42fe019 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7276,26 +7276,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 818e64706fa27450d1093148d75be01ef138e29e Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Mon, 9 Jun 2025 13:44:00 -0700 Subject: [PATCH 2/8] 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 11858bd68439b..3fa10abbcc818 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 26ad0f278939dbce0f62786210a593802a61b0a5 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sun, 6 Jul 2025 10:30:02 -0700 Subject: [PATCH 3/8] 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 3fa10abbcc818..6da3c9c8b492e 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 6114ea42fe019..64255cf220654 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7309,6 +7309,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 598f9f8b072c2247b14e9681be7df7073d37f463 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sun, 6 Jul 2025 11:06:45 -0700 Subject: [PATCH 4/8] 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 64255cf220654..38f9ddf724c90 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7276,6 +7276,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 @@ -7284,17 +7290,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); @@ -7310,12 +7314,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 9c82f852b7aa5e7fcae484a3e9d263d6dd1dc9ee Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 9 Jul 2025 16:07:59 -0700 Subject: [PATCH 5/8] 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 6da3c9c8b492e..5fc0ae584c23e 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 8c257c14fd86e..f85a0f8e92aa5 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -7519,11 +7519,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 3f3e2751eb7410d6efe92139e411b0030f76a283 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 9 Jul 2025 16:12:56 -0700 Subject: [PATCH 6/8] 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 00e54ce1e4aab6226c1f195daa58214c83dcf0f8 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 25 Jul 2025 10:47:21 -0700 Subject: [PATCH 7/8] 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 1978ce13786ee88aea7cd5b7e1760cfaa233d29b Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 25 Jul 2025 11:13:05 -0700 Subject: [PATCH 8/8] 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 5fc0ae584c23e..abb738ac05cb8 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 f85a0f8e92aa5..4aabd13a36495 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -7543,8 +7543,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); @@ -7552,6 +7553,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)) { @@ -7560,7 +7562,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(); @@ -8398,6 +8401,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; @@ -8427,15 +8434,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 @@ -8449,10 +8448,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; @@ -8487,9 +8482,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 38f9ddf724c90..499304533a82c 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 { @@ -1236,7 +1237,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; @@ -1249,6 +1250,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); @@ -1256,7 +1258,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); @@ -1269,7 +1272,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(); @@ -1301,7 +1304,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(); @@ -1310,7 +1313,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; @@ -1343,7 +1346,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(); } } @@ -2048,15 +2051,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); } /* }}} */ @@ -2898,7 +2907,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 */ @@ -4020,7 +4030,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); } /* }}} */ @@ -4425,7 +4436,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); } /* }}} */ @@ -6353,7 +6365,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); } /* }}} */ @@ -7319,13 +7332,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 @@ -7857,7 +7878,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)