Skip to content

Commit f87967f

Browse files
Delay validation of #[\NoDiscard] on property hooks
1 parent c3b46a3 commit f87967f

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
@@ -7568,15 +7568,17 @@ static void zend_compile_attributes(
75687568
continue;
75697569
}
75707570

7571-
if (delayed_target_validation == NULL) {
7572-
if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) {
7571+
bool run_validator = true;
7572+
if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) {
7573+
if (delayed_target_validation == NULL) {
75737574
zend_string *location = zend_get_attribute_target_names(target);
75747575
zend_string *allowed = zend_get_attribute_target_names(config->flags);
75757576

75767577
zend_error_noreturn(E_ERROR, "Attribute \"%s\" cannot target %s (allowed targets: %s)",
75777578
ZSTR_VAL(attr->name), ZSTR_VAL(location), ZSTR_VAL(allowed)
75787579
);
75797580
}
7581+
run_validator = false;
75807582
}
75817583

75827584
if (!(config->flags & ZEND_ATTRIBUTE_IS_REPEATABLE)) {
@@ -7585,7 +7587,8 @@ static void zend_compile_attributes(
75857587
}
75867588
}
75877589

7588-
if (config->validator != NULL) {
7590+
// Validators are not run if the target is already invalid
7591+
if (run_validator && config->validator != NULL) {
75897592
config->validator(attr, target | extra_flags, CG(active_class_entry));
75907593
}
75917594
} ZEND_HASH_FOREACH_END();
@@ -8427,6 +8430,10 @@ static zend_op_array *zend_compile_func_decl_ex(
84278430

84288431
CG(active_op_array) = op_array;
84298432

8433+
zend_oparray_context_begin(&orig_oparray_context, op_array);
8434+
CG(context).active_property_info_name = property_info_name;
8435+
CG(context).active_property_hook_kind = hook_kind;
8436+
84308437
if (decl->child[4]) {
84318438
int target = ZEND_ATTRIBUTE_TARGET_FUNCTION;
84328439

@@ -8456,15 +8463,7 @@ static zend_op_array *zend_compile_func_decl_ex(
84568463
op_array->fn_flags |= ZEND_ACC_DEPRECATED;
84578464
}
84588465

8459-
zend_attribute *nodiscard_attribute = zend_get_attribute_str(
8460-
op_array->attributes,
8461-
"nodiscard",
8462-
sizeof("nodiscard")-1
8463-
);
8464-
8465-
if (nodiscard_attribute) {
8466-
op_array->fn_flags |= ZEND_ACC_NODISCARD;
8467-
}
8466+
// ZEND_ACC_NODISCARD is added via an attribute validator
84688467
}
84698468

84708469
/* Do not leak the class scope into free standing functions, even if they are dynamically
@@ -8478,10 +8477,6 @@ static zend_op_array *zend_compile_func_decl_ex(
84788477
op_array->fn_flags |= ZEND_ACC_TOP_LEVEL;
84798478
}
84808479

8481-
zend_oparray_context_begin(&orig_oparray_context, op_array);
8482-
CG(context).active_property_info_name = property_info_name;
8483-
CG(context).active_property_hook_kind = hook_kind;
8484-
84858480
{
84868481
/* Push a separator to the loop variable stack */
84878482
zend_loop_var dummy_var;
@@ -8516,9 +8511,11 @@ static zend_op_array *zend_compile_func_decl_ex(
85168511
}
85178512

85188513
if (op_array->fn_flags & ZEND_ACC_NODISCARD) {
8519-
if (is_hook) {
8520-
zend_error_noreturn(E_COMPILE_ERROR, "#[\\NoDiscard] is not supported for property hooks");
8521-
}
8514+
/* ZEND_ACC_NODISCARD gets added by the attribute validator, but only
8515+
* if the method is not a hook; if it is a hook, then the validator
8516+
* should have either thrown an error or done nothing due to delayed
8517+
* target validation. */
8518+
ZEND_ASSERT(!is_hook);
85228519

85238520
if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
85248521
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 {
@@ -1254,7 +1255,7 @@ static void _extension_string(smart_str *str, const zend_module_entry *module, c
12541255

12551256
/* {{{ reflection_attribute_factory */
12561257
static void reflection_attribute_factory(zval *object, HashTable *attributes, zend_attribute *data,
1257-
zend_class_entry *scope, uint32_t target, zend_string *filename)
1258+
zend_class_entry *scope, uint32_t target, zend_string *filename, bool on_property_hook)
12581259
{
12591260
reflection_object *intern;
12601261
attribute_reference *reference;
@@ -1267,14 +1268,16 @@ static void reflection_attribute_factory(zval *object, HashTable *attributes, ze
12671268
reference->scope = scope;
12681269
reference->filename = filename ? zend_string_copy(filename) : NULL;
12691270
reference->target = target;
1271+
reference->on_property_hook = on_property_hook;
12701272
intern->ptr = reference;
12711273
intern->ref_type = REF_TYPE_ATTRIBUTE;
12721274
ZVAL_STR_COPY(reflection_prop_name(object), data->name);
12731275
}
12741276
/* }}} */
12751277

12761278
static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *scope,
1277-
uint32_t offset, uint32_t target, zend_string *name, zend_class_entry *base, zend_string *filename) /* {{{ */
1279+
uint32_t offset, uint32_t target, zend_string *name, zend_class_entry *base, zend_string *filename,
1280+
bool on_property_hook) /* {{{ */
12781281
{
12791282
ZEND_ASSERT(attributes != NULL);
12801283

@@ -1287,7 +1290,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s
12871290

12881291
ZEND_HASH_PACKED_FOREACH_PTR(attributes, attr) {
12891292
if (attr->offset == offset && zend_string_equals(attr->lcname, filter)) {
1290-
reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename);
1293+
reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename, on_property_hook);
12911294
add_next_index_zval(ret, &tmp);
12921295
}
12931296
} ZEND_HASH_FOREACH_END();
@@ -1319,7 +1322,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s
13191322
}
13201323
}
13211324

1322-
reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename);
1325+
reflection_attribute_factory(&tmp, attributes, attr, scope, target, filename, on_property_hook);
13231326
add_next_index_zval(ret, &tmp);
13241327
} ZEND_HASH_FOREACH_END();
13251328

@@ -1328,7 +1331,7 @@ static int read_attributes(zval *ret, HashTable *attributes, zend_class_entry *s
13281331
/* }}} */
13291332

13301333
static void reflect_attributes(INTERNAL_FUNCTION_PARAMETERS, HashTable *attributes,
1331-
uint32_t offset, zend_class_entry *scope, uint32_t target, zend_string *filename) /* {{{ */
1334+
uint32_t offset, zend_class_entry *scope, uint32_t target, zend_string *filename, bool on_property_hook) /* {{{ */
13321335
{
13331336
zend_string *name = NULL;
13341337
zend_long flags = 0;
@@ -1361,7 +1364,7 @@ static void reflect_attributes(INTERNAL_FUNCTION_PARAMETERS, HashTable *attribut
13611364

13621365
array_init(return_value);
13631366

1364-
if (FAILURE == read_attributes(return_value, attributes, scope, offset, target, name, base, filename)) {
1367+
if (FAILURE == read_attributes(return_value, attributes, scope, offset, target, name, base, filename, on_property_hook)) {
13651368
RETURN_THROWS();
13661369
}
13671370
}
@@ -2066,15 +2069,21 @@ ZEND_METHOD(ReflectionFunctionAbstract, getAttributes)
20662069

20672070
GET_REFLECTION_OBJECT_PTR(fptr);
20682071

2072+
bool on_property_hook = false;
2073+
20692074
if (fptr->common.scope && (fptr->common.fn_flags & (ZEND_ACC_CLOSURE|ZEND_ACC_FAKE_CLOSURE)) != ZEND_ACC_CLOSURE) {
20702075
target = ZEND_ATTRIBUTE_TARGET_METHOD;
2076+
if (fptr->common.prop_info != NULL) {
2077+
on_property_hook = true;
2078+
}
20712079
} else {
20722080
target = ZEND_ATTRIBUTE_TARGET_FUNCTION;
20732081
}
20742082

20752083
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
20762084
fptr->common.attributes, 0, fptr->common.scope, target,
2077-
fptr->type == ZEND_USER_FUNCTION ? fptr->op_array.filename : NULL);
2085+
fptr->type == ZEND_USER_FUNCTION ? fptr->op_array.filename : NULL,
2086+
on_property_hook);
20782087
}
20792088
/* }}} */
20802089

@@ -2916,7 +2925,8 @@ ZEND_METHOD(ReflectionParameter, getAttributes)
29162925

29172926
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
29182927
attributes, param->offset + 1, scope, ZEND_ATTRIBUTE_TARGET_PARAMETER,
2919-
param->fptr->type == ZEND_USER_FUNCTION ? param->fptr->op_array.filename : NULL);
2928+
param->fptr->type == ZEND_USER_FUNCTION ? param->fptr->op_array.filename : NULL,
2929+
false);
29202930
}
29212931

29222932
/* {{{ Returns the index of the parameter, starting from 0 */
@@ -4038,7 +4048,8 @@ ZEND_METHOD(ReflectionClassConstant, getAttributes)
40384048

40394049
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
40404050
ref->attributes, 0, ref->ce, ZEND_ATTRIBUTE_TARGET_CLASS_CONST,
4041-
ref->ce->type == ZEND_USER_CLASS ? ref->ce->info.user.filename : NULL);
4051+
ref->ce->type == ZEND_USER_CLASS ? ref->ce->info.user.filename : NULL,
4052+
false);
40424053
}
40434054
/* }}} */
40444055

@@ -4443,7 +4454,8 @@ ZEND_METHOD(ReflectionClass, getAttributes)
44434454

44444455
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
44454456
ce->attributes, 0, ce, ZEND_ATTRIBUTE_TARGET_CLASS,
4446-
ce->type == ZEND_USER_CLASS ? ce->info.user.filename : NULL);
4457+
ce->type == ZEND_USER_CLASS ? ce->info.user.filename : NULL,
4458+
false);
44474459
}
44484460
/* }}} */
44494461

@@ -6385,7 +6397,8 @@ ZEND_METHOD(ReflectionProperty, getAttributes)
63856397

63866398
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
63876399
ref->prop->attributes, 0, ref->prop->ce, ZEND_ATTRIBUTE_TARGET_PROPERTY,
6388-
ref->prop->ce->type == ZEND_USER_CLASS ? ref->prop->ce->info.user.filename : NULL);
6400+
ref->prop->ce->type == ZEND_USER_CLASS ? ref->prop->ce->info.user.filename : NULL,
6401+
false);
63896402
}
63906403
/* }}} */
63916404

@@ -7348,13 +7361,21 @@ ZEND_METHOD(ReflectionAttribute, newInstance)
73487361
if (config != NULL && config->validator != NULL) {
73497362
config->validator(
73507363
attr->data,
7351-
flags | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION,
7364+
attr->target | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION,
73527365
attr->scope
73537366
);
73547367
if (EG(exception)) {
73557368
RETURN_THROWS();
73567369
}
73577370
}
7371+
/* For #[NoDiscard], the attribute does not work on property hooks, but
7372+
* at runtime the validator has no way to access the method that an
7373+
* attribute is applied to, attr->scope is just the overall class entry
7374+
* that the method is a part of. */
7375+
if (ce == zend_ce_nodiscard && attr->on_property_hook) {
7376+
zend_throw_error(NULL, "#[\\NoDiscard] is not supported for property hooks");;
7377+
RETURN_THROWS();
7378+
}
73587379
}
73597380

73607381
/* Repetition validation is done even if #[DelayedTargetValidation] is used
@@ -7886,7 +7907,7 @@ ZEND_METHOD(ReflectionConstant, getAttributes)
78867907

78877908
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
78887909
const_->attributes, 0, NULL, ZEND_ATTRIBUTE_TARGET_CONST,
7889-
const_->filename);
7910+
const_->filename, false);
78907911
}
78917912

78927913
ZEND_METHOD(ReflectionConstant, __toString)

0 commit comments

Comments
 (0)