Skip to content

Commit 1978ce1

Browse files
Delay validation of #[\NoDiscard] on property hooks
1 parent 00e54ce commit 1978ce1

File tree

5 files changed

+127
-38
lines changed

5 files changed

+127
-38
lines changed

Zend/tests/attributes/delayed_target_validation/errors_from_validator.phpt

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ class DemoClass {
2626

2727
public string $hooked {
2828
#[DelayedTargetValidation]
29-
// #[NoDiscard] // Does nothing here
29+
#[NoDiscard] // Does nothing here
3030
get => $this->hooked;
3131
#[DelayedTargetValidation]
32-
// #[NoDiscard] // Does nothing here
32+
#[NoDiscard] // Does nothing here
3333
set => $value;
3434
}
3535
}
@@ -39,8 +39,8 @@ $cases = [
3939
new ReflectionClass('DemoInterface'),
4040
new ReflectionClass('DemoReadonly'),
4141
new ReflectionClass('DemoEnum'),
42-
// new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Get),
43-
// new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Set),
42+
new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Get),
43+
new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Set),
4444
];
4545
foreach ($cases as $r) {
4646
echo str_repeat("*", 20) . "\n";
@@ -195,3 +195,48 @@ array(2) {
195195
}
196196
}
197197
Error: Cannot apply #[AllowDynamicProperties] to enum DemoEnum
198+
********************
199+
Method [ <user> public method $hooked::get ] {
200+
@@ %s %d - %d
201+
202+
- Parameters [0] {
203+
}
204+
- Return [ string ]
205+
}
206+
207+
array(2) {
208+
[0]=>
209+
object(ReflectionAttribute)#%d (1) {
210+
["name"]=>
211+
string(23) "DelayedTargetValidation"
212+
}
213+
[1]=>
214+
object(ReflectionAttribute)#%d (1) {
215+
["name"]=>
216+
string(9) "NoDiscard"
217+
}
218+
}
219+
Error: #[\NoDiscard] is not supported for property hooks
220+
********************
221+
Method [ <user> public method $hooked::set ] {
222+
@@ %s %d - %d
223+
224+
- Parameters [1] {
225+
Parameter #0 [ <required> string $value ]
226+
}
227+
- Return [ void ]
228+
}
229+
230+
array(2) {
231+
[0]=>
232+
object(ReflectionAttribute)#%d (1) {
233+
["name"]=>
234+
string(23) "DelayedTargetValidation"
235+
}
236+
[1]=>
237+
object(ReflectionAttribute)#%d (1) {
238+
["name"]=>
239+
string(9) "NoDiscard"
240+
}
241+
}
242+
Error: #[\NoDiscard] is not supported for property hooks

Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ class DemoClass {
1212

1313
public string $hooked {
1414
#[DelayedTargetValidation]
15-
// #[NoDiscard] // Does nothing here
15+
#[NoDiscard] // Does nothing here
1616
get => $this->hooked;
1717
#[DelayedTargetValidation]
18-
// #[NoDiscard] // Does nothing here
18+
#[NoDiscard] // Does nothing here
1919
set => $value;
2020
}
2121

Zend/zend_attributes.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,31 @@ ZEND_METHOD(Deprecated, __construct)
210210
}
211211
}
212212

213+
static void validate_nodiscard(
214+
zend_attribute *attr, uint32_t target, zend_class_entry *scope)
215+
{
216+
/* There isn't an easy way to identify the *method* that the attribute is
217+
* applied to in a manner that works during both compilation (normal
218+
* validation) and runtime (delayed validation). So, handle them separately.
219+
*/
220+
if (CG(in_compilation)) {
221+
ZEND_ASSERT((target & ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION) == 0);
222+
zend_op_array *op_array = CG(active_op_array);
223+
const zend_string *prop_info_name = CG(context).active_property_info_name;
224+
if (prop_info_name == NULL) {
225+
op_array->fn_flags |= ZEND_ACC_NODISCARD;
226+
return;
227+
}
228+
// Applied to a hook, either throw or ignore
229+
if (target & ZEND_ATTRIBUTE_NO_TARGET_VALIDATION) {
230+
return;
231+
}
232+
zend_error_noreturn(E_COMPILE_ERROR, "#[\\NoDiscard] is not supported for property hooks");
233+
}
234+
/* At runtime, no way to identify the target method; Reflection will handle
235+
* throwing the error if needed. */
236+
}
237+
213238
ZEND_METHOD(NoDiscard, __construct)
214239
{
215240
zend_string *message = NULL;
@@ -569,6 +594,7 @@ void zend_register_attribute_ce(void)
569594

570595
zend_ce_nodiscard = register_class_NoDiscard();
571596
attr = zend_mark_internal_attribute(zend_ce_nodiscard);
597+
attr->validator = validate_nodiscard;
572598

573599
zend_ce_delayed_target_validation = register_class_DelayedTargetValidation();
574600
attr = zend_mark_internal_attribute(zend_ce_delayed_target_validation);

Zend/zend_compile.c

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7543,15 +7543,17 @@ static void zend_compile_attributes(
75437543
continue;
75447544
}
75457545

7546-
if (delayed_target_validation == NULL) {
7547-
if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) {
7546+
bool run_validator = true;
7547+
if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) {
7548+
if (delayed_target_validation == NULL) {
75487549
zend_string *location = zend_get_attribute_target_names(target);
75497550
zend_string *allowed = zend_get_attribute_target_names(config->flags);
75507551

75517552
zend_error_noreturn(E_ERROR, "Attribute \"%s\" cannot target %s (allowed targets: %s)",
75527553
ZSTR_VAL(attr->name), ZSTR_VAL(location), ZSTR_VAL(allowed)
75537554
);
75547555
}
7556+
run_validator = false;
75557557
}
75567558

75577559
if (!(config->flags & ZEND_ATTRIBUTE_IS_REPEATABLE)) {
@@ -7560,7 +7562,8 @@ static void zend_compile_attributes(
75607562
}
75617563
}
75627564

7563-
if (config->validator != NULL) {
7565+
// Validators are not run if the target is already invalid
7566+
if (run_validator && config->validator != NULL) {
75647567
config->validator(attr, target | extra_flags, CG(active_class_entry));
75657568
}
75667569
} ZEND_HASH_FOREACH_END();
@@ -8398,6 +8401,10 @@ static zend_op_array *zend_compile_func_decl_ex(
83988401

83998402
CG(active_op_array) = op_array;
84008403

8404+
zend_oparray_context_begin(&orig_oparray_context, op_array);
8405+
CG(context).active_property_info_name = property_info_name;
8406+
CG(context).active_property_hook_kind = hook_kind;
8407+
84018408
if (decl->child[4]) {
84028409
int target = ZEND_ATTRIBUTE_TARGET_FUNCTION;
84038410

@@ -8427,15 +8434,7 @@ static zend_op_array *zend_compile_func_decl_ex(
84278434
op_array->fn_flags |= ZEND_ACC_DEPRECATED;
84288435
}
84298436

8430-
zend_attribute *nodiscard_attribute = zend_get_attribute_str(
8431-
op_array->attributes,
8432-
"nodiscard",
8433-
sizeof("nodiscard")-1
8434-
);
8435-
8436-
if (nodiscard_attribute) {
8437-
op_array->fn_flags |= ZEND_ACC_NODISCARD;
8438-
}
8437+
// ZEND_ACC_NODISCARD is added via an attribute validator
84398438
}
84408439

84418440
/* 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(
84498448
op_array->fn_flags |= ZEND_ACC_TOP_LEVEL;
84508449
}
84518450

8452-
zend_oparray_context_begin(&orig_oparray_context, op_array);
8453-
CG(context).active_property_info_name = property_info_name;
8454-
CG(context).active_property_hook_kind = hook_kind;
8455-
84568451
{
84578452
/* Push a separator to the loop variable stack */
84588453
zend_loop_var dummy_var;
@@ -8487,9 +8482,11 @@ static zend_op_array *zend_compile_func_decl_ex(
84878482
}
84888483

84898484
if (op_array->fn_flags & ZEND_ACC_NODISCARD) {
8490-
if (is_hook) {
8491-
zend_error_noreturn(E_COMPILE_ERROR, "#[\\NoDiscard] is not supported for property hooks");
8492-
}
8485+
/* ZEND_ACC_NODISCARD gets added by the attribute validator, but only
8486+
* if the method is not a hook; if it is a hook, then the validator
8487+
* should have either thrown an error or done nothing due to delayed
8488+
* target validation. */
8489+
ZEND_ASSERT(!is_hook);
84938490

84948491
if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
84958492
zend_arg_info *return_info = CG(active_op_array)->arg_info - 1;

ext/reflection/php_reflection.c

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ typedef struct _attribute_reference {
157157
zend_class_entry *scope;
158158
zend_string *filename;
159159
uint32_t target;
160+
bool on_property_hook;
160161
} attribute_reference;
161162

162163
typedef enum {
@@ -1236,7 +1237,7 @@ static void _extension_string(smart_str *str, const zend_module_entry *module, c
12361237

12371238
/* {{{ reflection_attribute_factory */
12381239
static void reflection_attribute_factory(zval *object, HashTable *attributes, zend_attribute *data,
1239-
zend_class_entry *scope, uint32_t target, zend_string *filename)
1240+
zend_class_entry *scope, uint32_t target, zend_string *filename, bool on_property_hook)
12401241
{
12411242
reflection_object *intern;
12421243
attribute_reference *reference;
@@ -1249,14 +1250,16 @@ static void reflection_attribute_factory(zval *object, HashTable *attributes, ze
12491250
reference->scope = scope;
12501251
reference->filename = filename ? zend_string_copy(filename) : NULL;
12511252
reference->target = target;
1253+
reference->on_property_hook = on_property_hook;
12521254
intern->ptr = reference;
12531255
intern->ref_type = REF_TYPE_ATTRIBUTE;
12541256
ZVAL_STR_COPY(reflection_prop_name(object), data->name);
12551257
}
12561258
/* }}} */
12571259

12581260
static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *scope,
1259-
uint32_t offset, uint32_t target, zend_string *name, zend_class_entry *base, zend_string *filename) /* {{{ */
1261+
uint32_t offset, uint32_t target, zend_string *name, zend_class_entry *base, zend_string *filename,
1262+
bool on_property_hook) /* {{{ */
12601263
{
12611264
ZEND_ASSERT(attributes != NULL);
12621265

@@ -1269,7 +1272,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s
12691272

12701273
ZEND_HASH_PACKED_FOREACH_PTR(attributes, attr) {
12711274
if (attr->offset == offset && zend_string_equals(attr->lcname, filter)) {
1272-
reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename);
1275+
reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename, on_property_hook);
12731276
add_next_index_zval(ret, &tmp);
12741277
}
12751278
} ZEND_HASH_FOREACH_END();
@@ -1301,7 +1304,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s
13011304
}
13021305
}
13031306

1304-
reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename);
1307+
reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename, on_property_hook);
13051308
add_next_index_zval(ret, &tmp);
13061309
} ZEND_HASH_FOREACH_END();
13071310

@@ -1310,7 +1313,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s
13101313
/* }}} */
13111314

13121315
static void reflect_attributes(INTERNAL_FUNCTION_PARAMETERS, HashTable *attributes,
1313-
uint32_t offset, zend_class_entry *scope, uint32_t target, zend_string *filename) /* {{{ */
1316+
uint32_t offset, zend_class_entry *scope, uint32_t target, zend_string *filename, bool on_property_hook) /* {{{ */
13141317
{
13151318
zend_string *name = NULL;
13161319
zend_long flags = 0;
@@ -1343,7 +1346,7 @@ static void reflect_attributes(INTERNAL_FUNCTION_PARAMETERS, HashTable *attribut
13431346

13441347
array_init(return_value);
13451348

1346-
if (FAILURE == read_attributes(return_value, attributes, scope, offset, target, name, base, filename)) {
1349+
if (FAILURE == read_attributes(return_value, attributes, scope, offset, target, name, base, filename, on_property_hook)) {
13471350
RETURN_THROWS();
13481351
}
13491352
}
@@ -2048,15 +2051,21 @@ ZEND_METHOD(ReflectionFunctionAbstract, getAttributes)
20482051

20492052
GET_REFLECTION_OBJECT_PTR(fptr);
20502053

2054+
bool on_property_hook = false;
2055+
20512056
if (fptr->common.scope && (fptr->common.fn_flags & (ZEND_ACC_CLOSURE|ZEND_ACC_FAKE_CLOSURE)) != ZEND_ACC_CLOSURE) {
20522057
target = ZEND_ATTRIBUTE_TARGET_METHOD;
2058+
if (fptr->common.prop_info != NULL) {
2059+
on_property_hook = true;
2060+
}
20532061
} else {
20542062
target = ZEND_ATTRIBUTE_TARGET_FUNCTION;
20552063
}
20562064

20572065
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
20582066
fptr->common.attributes, 0, fptr->common.scope, target,
2059-
fptr->type == ZEND_USER_FUNCTION ? fptr->op_array.filename : NULL);
2067+
fptr->type == ZEND_USER_FUNCTION ? fptr->op_array.filename : NULL,
2068+
on_property_hook);
20602069
}
20612070
/* }}} */
20622071

@@ -2898,7 +2907,8 @@ ZEND_METHOD(ReflectionParameter, getAttributes)
28982907

28992908
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
29002909
attributes, param->offset + 1, scope, ZEND_ATTRIBUTE_TARGET_PARAMETER,
2901-
param->fptr->type == ZEND_USER_FUNCTION ? param->fptr->op_array.filename : NULL);
2910+
param->fptr->type == ZEND_USER_FUNCTION ? param->fptr->op_array.filename : NULL,
2911+
false);
29022912
}
29032913

29042914
/* {{{ Returns the index of the parameter, starting from 0 */
@@ -4020,7 +4030,8 @@ ZEND_METHOD(ReflectionClassConstant, getAttributes)
40204030

40214031
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
40224032
ref->attributes, 0, ref->ce, ZEND_ATTRIBUTE_TARGET_CLASS_CONST,
4023-
ref->ce->type == ZEND_USER_CLASS ? ref->ce->info.user.filename : NULL);
4033+
ref->ce->type == ZEND_USER_CLASS ? ref->ce->info.user.filename : NULL,
4034+
false);
40244035
}
40254036
/* }}} */
40264037

@@ -4425,7 +4436,8 @@ ZEND_METHOD(ReflectionClass, getAttributes)
44254436

44264437
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
44274438
ce->attributes, 0, ce, ZEND_ATTRIBUTE_TARGET_CLASS,
4428-
ce->type == ZEND_USER_CLASS ? ce->info.user.filename : NULL);
4439+
ce->type == ZEND_USER_CLASS ? ce->info.user.filename : NULL,
4440+
false);
44294441
}
44304442
/* }}} */
44314443

@@ -6353,7 +6365,8 @@ ZEND_METHOD(ReflectionProperty, getAttributes)
63536365

63546366
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
63556367
ref->prop->attributes, 0, ref->prop->ce, ZEND_ATTRIBUTE_TARGET_PROPERTY,
6356-
ref->prop->ce->type == ZEND_USER_CLASS ? ref->prop->ce->info.user.filename : NULL);
6368+
ref->prop->ce->type == ZEND_USER_CLASS ? ref->prop->ce->info.user.filename : NULL,
6369+
false);
63576370
}
63586371
/* }}} */
63596372

@@ -7319,13 +7332,21 @@ ZEND_METHOD(ReflectionAttribute, newInstance)
73197332
if (config != NULL && config->validator != NULL) {
73207333
config->validator(
73217334
attr->data,
7322-
flags | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION,
7335+
attr->target | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION,
73237336
attr->scope
73247337
);
73257338
if (EG(exception)) {
73267339
RETURN_THROWS();
73277340
}
73287341
}
7342+
/* For #[NoDiscard], the attribute does not work on property hooks, but
7343+
* at runtime the validator has no way to access the method that an
7344+
* attribute is applied to, attr->scope is just the overall class entry
7345+
* that the method is a part of. */
7346+
if (ce == zend_ce_nodiscard && attr->on_property_hook) {
7347+
zend_throw_error(NULL, "#[\\NoDiscard] is not supported for property hooks");;
7348+
RETURN_THROWS();
7349+
}
73297350
}
73307351

73317352
/* Repetition validation is done even if #[DelayedTargetValidation] is used
@@ -7857,7 +7878,7 @@ ZEND_METHOD(ReflectionConstant, getAttributes)
78577878

78587879
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
78597880
const_->attributes, 0, NULL, ZEND_ATTRIBUTE_TARGET_CONST,
7860-
const_->filename);
7881+
const_->filename, false);
78617882
}
78627883

78637884
ZEND_METHOD(ReflectionConstant, __toString)

0 commit comments

Comments
 (0)