Skip to content

Commit 3e2eac9

Browse files
committed
Implement parameter name inheritance
1 parent bb64585 commit 3e2eac9

File tree

8 files changed

+231
-3
lines changed

8 files changed

+231
-3
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Parameter name conflict: Direct inheritance conflict
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
public function method($a, $b) {}
8+
}
9+
class B extends A {
10+
public function method($b, $a) {}
11+
}
12+
13+
?>
14+
--EXPECTF--
15+
Fatal error: Parameter $a of B::method() at position #2 conflicts with parameter $a of A::method() at position #1 in %s on line %d
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
Parameter name conflict: Indirect conflict between two interfaces
3+
--FILE--
4+
<?php
5+
6+
interface I {
7+
public function method($a, $b);
8+
}
9+
interface J {
10+
public function method($b, $a);
11+
}
12+
class C implements I, J {
13+
public function method($x, $y) {}
14+
}
15+
16+
?>
17+
--EXPECTF--
18+
Fatal error: Parameter $b of C::method() at position #2 (inherited from a different prototype method) conflicts with parameter $b of J::method() at position #1 in %s on line %d
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
--TEST--
2+
Can use parameter names from proto methods
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
public function method($a) {
8+
echo __METHOD__ . ": $a\n";
9+
}
10+
}
11+
class B extends A {
12+
public function method($b) {
13+
echo __METHOD__ . ": $b\n";
14+
}
15+
}
16+
class C extends B {
17+
public function method($c) {
18+
echo __METHOD__ . ": $c\n";
19+
}
20+
}
21+
22+
(new B)->method(a: 42);
23+
(new B)->method(b: 42);
24+
(new C)->method(a: 42);
25+
(new C)->method(b: 42);
26+
(new C)->method(c: 42);
27+
echo "\n";
28+
29+
interface I {
30+
public function method($i);
31+
}
32+
interface J {
33+
public function method($j);
34+
}
35+
interface K {
36+
public function method($k);
37+
}
38+
class D implements I, J, K {
39+
public function method($d) {
40+
echo __METHOD__ . ": $d\n";
41+
}
42+
}
43+
44+
(new D)->method(i: 42);
45+
(new D)->method(j: 42);
46+
(new D)->method(k: 42);
47+
(new D)->method(d: 42);
48+
49+
?>
50+
--EXPECT--
51+
B::method: 42
52+
B::method: 42
53+
C::method: 42
54+
C::method: 42
55+
C::method: 42
56+
57+
D::method: 42
58+
D::method: 42
59+
D::method: 42
60+
D::method: 42

Zend/zend_API.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2092,6 +2092,8 @@ ZEND_API int zend_register_functions(zend_class_entry *scope, const zend_functio
20922092
internal_function->function_name = zend_string_init_interned(ptr->fname, fname_len, 1);
20932093
internal_function->scope = scope;
20942094
internal_function->prototype = NULL;
2095+
internal_function->attributes = NULL;
2096+
internal_function->arg_names = NULL;
20952097
if (ptr->flags) {
20962098
if (!(ptr->flags & ZEND_ACC_PPP_MASK)) {
20972099
if (ptr->flags != ZEND_ACC_DEPRECATED && scope) {

Zend/zend_compile.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ struct _zend_op_array {
418418
uint32_t required_num_args;
419419
zend_arg_info *arg_info;
420420
HashTable *attributes;
421+
HashTable *arg_names;
421422
/* END of common elements */
422423

423424
int cache_size; /* number of run_time_cache_slots * sizeof(void*) */
@@ -468,6 +469,7 @@ typedef struct _zend_internal_function {
468469
uint32_t required_num_args;
469470
zend_internal_arg_info *arg_info;
470471
HashTable *attributes;
472+
HashTable *arg_names;
471473
/* END of common elements */
472474

473475
zif_handler handler;
@@ -491,7 +493,14 @@ union _zend_function {
491493
uint32_t num_args;
492494
uint32_t required_num_args;
493495
zend_arg_info *arg_info; /* index -1 represents the return value info, if any */
494-
HashTable *attributes;
496+
HashTable *attributes;
497+
/* Mapping from argument names to argument offset for named arguments.
498+
* To reduce memory usage, this is only used if either:
499+
* * We need to preserve argument names from a parent method. (TODO)
500+
* * There are many arguments. (TODO)
501+
* Otherwise, we will simplify perform a linear scan over the arg_info.
502+
*/
503+
HashTable *arg_names;
495504
} common;
496505

497506
zend_op_array op_array;

Zend/zend_execute.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ ZEND_API const zend_internal_function zend_pass_function = {
138138
0, /* required_num_args */
139139
NULL, /* arg_info */
140140
NULL, /* attributes */
141+
NULL, /* arg_names */
141142
ZEND_FN(pass), /* handler */
142143
NULL, /* module */
143144
{NULL,NULL,NULL,NULL} /* reserved */
@@ -4294,7 +4295,16 @@ static zend_always_inline uint32_t zend_get_arg_offset_by_name(
42944295
return *(uintptr_t *)(cache_slot + 1);
42954296
}
42964297

4297-
// TODO: Use a hash table?
4298+
if (fbc->common.arg_names) {
4299+
zval *zv = zend_hash_find(fbc->common.arg_names, arg_name);
4300+
if (zv) {
4301+
*cache_slot = fbc;
4302+
*(uintptr_t *)(cache_slot + 1) = Z_LVAL_P(zv);
4303+
return Z_LVAL_P(zv);
4304+
}
4305+
return (uint32_t) - 1;
4306+
}
4307+
42984308
uint32_t num_args = fbc->common.num_args;
42994309
if (fbc->type == ZEND_USER_FUNCTION) {
43004310
for (uint32_t i = 0; i < num_args; i++) {

Zend/zend_inheritance.c

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,93 @@ static inheritance_status zend_do_perform_arg_type_hint_check(
524524
}
525525
/* }}} */
526526

527+
static const char *get_arg_name(const zend_function *func, const zend_arg_info *info, size_t *len) {
528+
if (func->type == ZEND_INTERNAL_FUNCTION) {
529+
zend_internal_arg_info *internal_info = (zend_internal_arg_info *) info;
530+
*len = strlen(internal_info->name);
531+
return internal_info->name;
532+
} else {
533+
*len = ZSTR_LEN(info->name);
534+
return ZSTR_VAL(info->name);
535+
}
536+
}
537+
538+
static void add_arg_name(
539+
HashTable *ht, const zend_function *to, const zend_function *from,
540+
zend_string *arg_name, uint32_t arg_offset) {
541+
zval *existing_arg_offset_zv = zend_hash_find(ht, arg_name);
542+
if (existing_arg_offset_zv) {
543+
uint32_t existing_arg_offset = Z_LVAL_P(existing_arg_offset_zv);
544+
if (existing_arg_offset != arg_offset) {
545+
/* Determine whether the conflict is with "to" itself, or because we inherited from
546+
* a different prototype method. */
547+
/* TODO: Determine the exact conflicting interface/method */
548+
size_t to_arg_name_len;
549+
const char *to_arg_name =
550+
get_arg_name(to, &to->common.arg_info[existing_arg_offset], &to_arg_name_len);
551+
zend_bool is_direct_conflict =
552+
ZSTR_LEN(arg_name) == to_arg_name_len
553+
&& !memcmp(ZSTR_VAL(arg_name), to_arg_name, to_arg_name_len);
554+
zend_error_noreturn(E_COMPILE_ERROR,
555+
"Parameter $%s of %s::%s() at position #%" PRIu32 "%s conflicts with "
556+
"parameter $%s of %s::%s() at position #%" PRIu32,
557+
ZSTR_VAL(arg_name),
558+
ZSTR_VAL(to->common.scope->name), ZSTR_VAL(to->common.function_name),
559+
existing_arg_offset + 1,
560+
is_direct_conflict ? "" : " (inherited from a different prototype method)",
561+
ZSTR_VAL(arg_name),
562+
ZSTR_VAL(from->common.scope->name), ZSTR_VAL(from->common.function_name),
563+
arg_offset + 1);
564+
}
565+
} else {
566+
zval arg_offset_zv;
567+
ZVAL_LONG(&arg_offset_zv, arg_offset);
568+
zend_hash_add_new(ht, arg_name, &arg_offset_zv);
569+
}
570+
}
571+
572+
static void add_arg_names(zend_function *to, const zend_function *from) {
573+
zend_bool persistent = to->type == ZEND_INTERNAL_FUNCTION;
574+
HashTable *ht = to->common.arg_names;
575+
zend_string *name;
576+
zval *val;
577+
578+
if (!ht) {
579+
ht = pemalloc(sizeof(HashTable), persistent);
580+
zend_hash_init(ht, to->common.num_args, NULL, NULL, persistent);
581+
}
582+
583+
if (from->common.arg_names) {
584+
ZEND_HASH_FOREACH_STR_KEY_VAL(from->common.arg_names, name, val) {
585+
uint32_t arg_offset = Z_LVAL_P(val);
586+
if (arg_offset >= to->common.num_args) {
587+
/* Don't inherit argument names that have been subsumed by a variadic. */
588+
continue;
589+
}
590+
591+
add_arg_name(ht, to, from, name, arg_offset);
592+
} ZEND_HASH_FOREACH_END();
593+
} else {
594+
for (uint32_t i = 0; i < from->common.num_args; i++) {
595+
if (i >= to->common.num_args) {
596+
/* Don't inherit argument names that have been subsumed by a variadic. */
597+
continue;
598+
}
599+
600+
const zend_arg_info *info = &from->common.arg_info[i];
601+
const zend_internal_arg_info *internal_info = (const zend_internal_arg_info *) info;
602+
zend_string *arg_name = from->type == ZEND_INTERNAL_FUNCTION
603+
? zend_string_init(internal_info->name, strlen(internal_info->name), persistent)
604+
: zend_string_copy(info->name);
605+
add_arg_name(ht, to, from, arg_name, i);
606+
zend_string_release(arg_name);
607+
}
608+
}
609+
if (!to->common.arg_names) {
610+
to->common.arg_names = ht;
611+
}
612+
}
613+
527614
/* For abstract trait methods, proto_scope will differ from proto->common.scope,
528615
* as self will refer to the self of the class the trait is used in, not the trait
529616
* the method was declared in. */
@@ -533,7 +620,7 @@ static inheritance_status zend_do_perform_implementation_check(
533620
{
534621
uint32_t i, num_args, proto_num_args, fe_num_args;
535622
inheritance_status status, local_status;
536-
zend_bool proto_is_variadic, fe_is_variadic;
623+
zend_bool proto_is_variadic, fe_is_variadic, needs_arg_name_table;
537624

538625
/* Checks for constructors only if they are declared in an interface,
539626
* or explicitly marked as abstract
@@ -570,6 +657,7 @@ static inheritance_status zend_do_perform_implementation_check(
570657
proto_num_args = proto->common.num_args + proto_is_variadic;
571658
fe_num_args = fe->common.num_args + fe_is_variadic;
572659
num_args = MAX(proto_num_args, fe_num_args);
660+
needs_arg_name_table = proto->common.arg_names != NULL;
573661

574662
status = INHERITANCE_SUCCESS;
575663
for (i = 0; i < num_args; i++) {
@@ -605,6 +693,27 @@ static inheritance_status zend_do_perform_implementation_check(
605693
if (ZEND_ARG_SEND_MODE(fe_arg_info) != ZEND_ARG_SEND_MODE(proto_arg_info)) {
606694
return INHERITANCE_ERROR;
607695
}
696+
697+
/* Only check this for non-variadic parameters. */
698+
if (i < fe->common.num_args && !needs_arg_name_table) {
699+
size_t proto_arg_name_len, fe_arg_name_len;
700+
const char *proto_arg_name = get_arg_name(proto, proto_arg_info, &proto_arg_name_len);
701+
const char *fe_arg_name = get_arg_name(fe, fe_arg_info, &fe_arg_name_len);
702+
if (proto_arg_name_len != fe_arg_name_len
703+
|| memcmp(proto_arg_name, fe_arg_name, fe_arg_name_len) != 0) {
704+
needs_arg_name_table = 1;
705+
}
706+
}
707+
}
708+
709+
if (needs_arg_name_table) {
710+
/* Parameter names are not the same! */
711+
/* TODO: HACK!!! */
712+
zend_function *fe_ = (zend_function *) fe;
713+
if (!fe->common.arg_names) {
714+
add_arg_names(fe_, fe);
715+
}
716+
add_arg_names(fe_, proto);
608717
}
609718

610719
/* Check return type compatibility, but only if the prototype already specifies

Zend/zend_opcode.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ void init_op_array(zend_op_array *op_array, zend_uchar type, int initial_ops_siz
6464
op_array->filename = zend_get_compiled_filename();
6565
op_array->doc_comment = NULL;
6666
op_array->attributes = NULL;
67+
op_array->arg_names = NULL;
6768

6869
op_array->arg_info = NULL;
6970
op_array->num_args = 0;
@@ -502,6 +503,10 @@ ZEND_API void destroy_op_array(zend_op_array *op_array)
502503
if (op_array->attributes) {
503504
zend_hash_release(op_array->attributes);
504505
}
506+
if (op_array->arg_names) {
507+
zend_hash_destroy(op_array->arg_names);
508+
efree(op_array->arg_names);
509+
}
505510
if (op_array->live_range) {
506511
efree(op_array->live_range);
507512
}

0 commit comments

Comments
 (0)