Skip to content

Commit 120eb26

Browse files
Delay validation of #[\NoDiscard] on property hooks
1 parent 55b4656 commit 120eb26

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
@@ -228,6 +228,31 @@ ZEND_METHOD(Deprecated, __construct)
228228
}
229229
}
230230

231+
static void validate_nodiscard(
232+
zend_attribute *attr, uint32_t target, zend_class_entry *scope)
233+
{
234+
/* There isn't an easy way to identify the *method* that the attribute is
235+
* applied to in a manner that works during both compilation (normal
236+
* validation) and runtime (delayed validation). So, handle them separately.
237+
*/
238+
if (CG(in_compilation)) {
239+
ZEND_ASSERT((target & ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION) == 0);
240+
zend_op_array *op_array = CG(active_op_array);
241+
const zend_string *prop_info_name = CG(context).active_property_info_name;
242+
if (prop_info_name == NULL) {
243+
op_array->fn_flags |= ZEND_ACC_NODISCARD;
244+
return;
245+
}
246+
// Applied to a hook, either throw or ignore
247+
if (target & ZEND_ATTRIBUTE_NO_TARGET_VALIDATION) {
248+
return;
249+
}
250+
zend_error_noreturn(E_COMPILE_ERROR, "#[\\NoDiscard] is not supported for property hooks");
251+
}
252+
/* At runtime, no way to identify the target method; Reflection will handle
253+
* throwing the error if needed. */
254+
}
255+
231256
ZEND_METHOD(NoDiscard, __construct)
232257
{
233258
zend_string *message = NULL;
@@ -588,6 +613,7 @@ void zend_register_attribute_ce(void)
588613

589614
zend_ce_nodiscard = register_class_NoDiscard();
590615
attr = zend_mark_internal_attribute(zend_ce_nodiscard);
616+
attr->validator = validate_nodiscard;
591617

592618
zend_ce_delayed_target_validation = register_class_DelayedTargetValidation();
593619
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
@@ -7579,15 +7579,17 @@ static void zend_compile_attributes(
75797579
continue;
75807580
}
75817581

7582-
if (delayed_target_validation == NULL) {
7583-
if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) {
7582+
bool run_validator = true;
7583+
if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) {
7584+
if (delayed_target_validation == NULL) {
75847585
zend_string *location = zend_get_attribute_target_names(target);
75857586
zend_string *allowed = zend_get_attribute_target_names(config->flags);
75867587

75877588
zend_error_noreturn(E_ERROR, "Attribute \"%s\" cannot target %s (allowed targets: %s)",
75887589
ZSTR_VAL(attr->name), ZSTR_VAL(location), ZSTR_VAL(allowed)
75897590
);
75907591
}
7592+
run_validator = false;
75917593
}
75927594

75937595
if (!(config->flags & ZEND_ATTRIBUTE_IS_REPEATABLE)) {
@@ -7596,7 +7598,8 @@ static void zend_compile_attributes(
75967598
}
75977599
}
75987600

7599-
if (config->validator != NULL) {
7601+
// Validators are not run if the target is already invalid
7602+
if (run_validator && config->validator != NULL) {
76007603
config->validator(attr, target | extra_flags, CG(active_class_entry));
76017604
}
76027605
} ZEND_HASH_FOREACH_END();
@@ -8443,6 +8446,10 @@ static zend_op_array *zend_compile_func_decl_ex(
84438446

84448447
CG(active_op_array) = op_array;
84458448

8449+
zend_oparray_context_begin(&orig_oparray_context, op_array);
8450+
CG(context).active_property_info_name = property_info_name;
8451+
CG(context).active_property_hook_kind = hook_kind;
8452+
84468453
if (decl->child[4]) {
84478454
int target = ZEND_ATTRIBUTE_TARGET_FUNCTION;
84488455

@@ -8472,15 +8479,7 @@ static zend_op_array *zend_compile_func_decl_ex(
84728479
op_array->fn_flags |= ZEND_ACC_DEPRECATED;
84738480
}
84748481

8475-
zend_attribute *nodiscard_attribute = zend_get_attribute_str(
8476-
op_array->attributes,
8477-
"nodiscard",
8478-
sizeof("nodiscard")-1
8479-
);
8480-
8481-
if (nodiscard_attribute) {
8482-
op_array->fn_flags |= ZEND_ACC_NODISCARD;
8483-
}
8482+
// ZEND_ACC_NODISCARD is added via an attribute validator
84848483
}
84858484

84868485
/* Do not leak the class scope into free standing functions, even if they are dynamically
@@ -8494,10 +8493,6 @@ static zend_op_array *zend_compile_func_decl_ex(
84948493
op_array->fn_flags |= ZEND_ACC_TOP_LEVEL;
84958494
}
84968495

8497-
zend_oparray_context_begin(&orig_oparray_context, op_array);
8498-
CG(context).active_property_info_name = property_info_name;
8499-
CG(context).active_property_hook_kind = hook_kind;
8500-
85018496
{
85028497
/* Push a separator to the loop variable stack */
85038498
zend_loop_var dummy_var;
@@ -8532,9 +8527,11 @@ static zend_op_array *zend_compile_func_decl_ex(
85328527
}
85338528

85348529
if (op_array->fn_flags & ZEND_ACC_NODISCARD) {
8535-
if (is_hook) {
8536-
zend_error_noreturn(E_COMPILE_ERROR, "#[\\NoDiscard] is not supported for property hooks");
8537-
}
8530+
/* ZEND_ACC_NODISCARD gets added by the attribute validator, but only
8531+
* if the method is not a hook; if it is a hook, then the validator
8532+
* should have either thrown an error or done nothing due to delayed
8533+
* target validation. */
8534+
ZEND_ASSERT(!is_hook);
85388535

85398536
if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
85408537
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

@@ -6390,7 +6402,8 @@ ZEND_METHOD(ReflectionProperty, getAttributes)
63906402

63916403
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
63926404
ref->prop->attributes, 0, ref->prop->ce, ZEND_ATTRIBUTE_TARGET_PROPERTY,
6393-
ref->prop->ce->type == ZEND_USER_CLASS ? ref->prop->ce->info.user.filename : NULL);
6405+
ref->prop->ce->type == ZEND_USER_CLASS ? ref->prop->ce->info.user.filename : NULL,
6406+
false);
63946407
}
63956408
/* }}} */
63966409

@@ -7364,13 +7377,21 @@ ZEND_METHOD(ReflectionAttribute, newInstance)
73647377
if (config != NULL && config->validator != NULL) {
73657378
config->validator(
73667379
attr->data,
7367-
flags | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION,
7380+
attr->target | ZEND_ATTRIBUTE_DELAYED_TARGET_VALIDATION,
73687381
attr->scope
73697382
);
73707383
if (EG(exception)) {
73717384
RETURN_THROWS();
73727385
}
73737386
}
7387+
/* For #[NoDiscard], the attribute does not work on property hooks, but
7388+
* at runtime the validator has no way to access the method that an
7389+
* attribute is applied to, attr->scope is just the overall class entry
7390+
* that the method is a part of. */
7391+
if (ce == zend_ce_nodiscard && attr->on_property_hook) {
7392+
zend_throw_error(NULL, "#[\\NoDiscard] is not supported for property hooks");;
7393+
RETURN_THROWS();
7394+
}
73747395
}
73757396

73767397
/* Repetition validation is done even if #[DelayedTargetValidation] is used
@@ -7902,7 +7923,7 @@ ZEND_METHOD(ReflectionConstant, getAttributes)
79027923

79037924
reflect_attributes(INTERNAL_FUNCTION_PARAM_PASSTHRU,
79047925
const_->attributes, 0, NULL, ZEND_ATTRIBUTE_TARGET_CONST,
7905-
const_->filename);
7926+
const_->filename, false);
79067927
}
79077928

79087929
ZEND_METHOD(ReflectionConstant, __toString)

0 commit comments

Comments
 (0)