diff --git a/Zend/tests/type_declarations/typed_properties_095.phpt b/Zend/tests/type_declarations/typed_properties_095.phpt index 5caf862e72da..321b07e34c73 100644 --- a/Zend/tests/type_declarations/typed_properties_095.phpt +++ b/Zend/tests/type_declarations/typed_properties_095.phpt @@ -75,6 +75,8 @@ object(_ZendTestClass)#1 (3) { uninitialized(Traversable&Countable) ["readonlyProp"]=> uninitialized(int) + ["dnfProperty"]=> + uninitialized(Iterator|(Traversable&Countable)) } int(123) Cannot assign string to property _ZendTestClass::$intProp of type int @@ -91,6 +93,8 @@ object(Test)#4 (3) { uninitialized(Traversable&Countable) ["readonlyProp"]=> uninitialized(int) + ["dnfProperty"]=> + uninitialized(Iterator|(Traversable&Countable)) } int(123) Cannot assign string to property _ZendTestClass::$staticIntProp of type int diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 34ab803321aa..d0b863335e29 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -2756,6 +2756,28 @@ ZEND_API void zend_add_magic_method(zend_class_entry *ce, zend_function *fptr, z ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arg_info_toString, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() +static zend_always_inline void zend_normalize_internal_type(zend_type *type) { + ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*type)); + zend_type *current; + ZEND_TYPE_FOREACH(*type, current) { + if (ZEND_TYPE_HAS_NAME(*current)) { + zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*current)); + zend_alloc_ce_cache(name); + ZEND_TYPE_SET_PTR(*current, name); + } else if (ZEND_TYPE_HAS_LIST(*current)) { + zend_type *inner; + ZEND_TYPE_FOREACH(*current, inner) { + ZEND_ASSERT(!ZEND_TYPE_HAS_LITERAL_NAME(*inner) && !ZEND_TYPE_HAS_LIST(*inner)); + if (ZEND_TYPE_HAS_NAME(*inner)) { + zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*inner)); + zend_alloc_ce_cache(name); + ZEND_TYPE_SET_PTR(*inner, name); + } + } ZEND_TYPE_FOREACH_END(); + } + } ZEND_TYPE_FOREACH_END(); +} + /* registers all functions in *library_functions in the function hash */ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend_function_entry *functions, HashTable *function_table, int type) /* {{{ */ { @@ -2934,10 +2956,12 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend memcpy(new_arg_info, arg_info, sizeof(zend_internal_arg_info) * num_args); reg_function->arg_info = new_arg_info + 1; for (i = 0; i < num_args; i++) { - if (ZEND_TYPE_IS_COMPLEX(new_arg_info[i].type)) { - ZEND_ASSERT(ZEND_TYPE_HAS_NAME(new_arg_info[i].type) - && "Should be stored as simple name"); + if (ZEND_TYPE_HAS_LITERAL_NAME(new_arg_info[i].type)) { + // gen_stubs.php does not support codegen for DNF types in arg infos. + // As a temporary workaround, we split the type name on `|` characters, + // converting it to an union type if necessary. const char *class_name = ZEND_TYPE_LITERAL_NAME(new_arg_info[i].type); + new_arg_info[i].type.type_mask &= ~_ZEND_TYPE_LITERAL_NAME_BIT; size_t num_types = 1; const char *p = class_name; @@ -2948,8 +2972,10 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend if (num_types == 1) { /* Simple class type */ - ZEND_TYPE_SET_PTR(new_arg_info[i].type, - zend_string_init_interned(class_name, strlen(class_name), 1)); + zend_string *str = zend_string_init_interned(class_name, strlen(class_name), 1); + zend_alloc_ce_cache(str); + ZEND_TYPE_SET_PTR(new_arg_info[i].type, str); + new_arg_info[i].type.type_mask |= _ZEND_TYPE_NAME_BIT; } else { /* Union type */ zend_type_list *list = malloc(ZEND_TYPE_LIST_SIZE(num_types)); @@ -2961,8 +2987,8 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend uint32_t j = 0; while (true) { const char *end = strchr(start, '|'); - zend_string *str = zend_string_init_interned( - start, end ? end - start : strlen(start), 1); + zend_string *str = zend_string_init_interned(start, end ? end - start : strlen(start), 1); + zend_alloc_ce_cache(str); list->types[j] = (zend_type) ZEND_TYPE_INIT_CLASS(str, 0, 0); if (!end) { break; @@ -2977,10 +3003,14 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend zend_error(E_CORE_WARNING, "iterable type is now a compile time alias for array|Traversable," " regenerate the argument info via the php-src gen_stub build script"); */ - zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE), - (new_arg_info[i].type.type_mask|MAY_BE_ARRAY)); + zend_type legacy_iterable = ZEND_TYPE_INIT_CLASS_MASK( + ZSTR_KNOWN(ZEND_STR_TRAVERSABLE), + (new_arg_info[i].type.type_mask | MAY_BE_ARRAY) + ); new_arg_info[i].type = legacy_iterable; } + + zend_normalize_internal_type(&new_arg_info[i].type); } } @@ -4367,16 +4397,7 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z property_info->type = type; if (is_persistent_class(ce)) { - zend_type *single_type; - ZEND_TYPE_FOREACH(property_info->type, single_type) { - // TODO Add support and test cases when gen_stub support added - ZEND_ASSERT(!ZEND_TYPE_HAS_LIST(*single_type)); - if (ZEND_TYPE_HAS_NAME(*single_type)) { - zend_string *name = zend_new_interned_string(ZEND_TYPE_NAME(*single_type)); - ZEND_TYPE_SET_PTR(*single_type, name); - zend_alloc_ce_cache(name); - } - } ZEND_TYPE_FOREACH_END(); + zend_normalize_internal_type(&property_info->type); } zend_hash_update_ptr(&ce->properties_info, name, property_info); diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 5dff47798b01..5ba58aefe210 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -6402,7 +6402,7 @@ static zend_type zend_compile_single_typename(zend_ast *ast) /* Transform iterable into a type union alias */ if (type_code == IS_ITERABLE) { /* Set iterable bit for BC compat during Reflection and string representation of type */ - zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_CONST_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE), + zend_type iterable = (zend_type) ZEND_TYPE_INIT_CLASS_MASK(ZSTR_KNOWN(ZEND_STR_TRAVERSABLE), (MAY_BE_ARRAY|_ZEND_TYPE_ITERABLE_BIT)); return iterable; } diff --git a/Zend/zend_types.h b/Zend/zend_types.h index df9bff9015ea..bf4fd9d18a41 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -115,6 +115,7 @@ typedef void (*copy_ctor_func_t)(zval *pElement); * ZEND_TYPE_IS_ONLY_MASK() - checks if type-hint refer to standard type only * ZEND_TYPE_IS_COMPLEX() - checks if type is a type_list, or contains a class either as a CE or as a name * ZEND_TYPE_HAS_NAME() - checks if type-hint contains some class as zend_string * + * ZEND_TYPE_HAS_LITERAL_NAME() - checks if type-hint contains some class as const char * * ZEND_TYPE_IS_INTERSECTION() - checks if the type_list represents an intersection type list * ZEND_TYPE_IS_UNION() - checks if the type_list represents a union type list * @@ -145,8 +146,10 @@ typedef struct { #define _ZEND_TYPE_MASK ((1u << 25) - 1) /* Only one of these bits may be set. */ #define _ZEND_TYPE_NAME_BIT (1u << 24) +// Used to signify that type.ptr is not a `zend_string*` but a `const char*`, +#define _ZEND_TYPE_LITERAL_NAME_BIT (1u << 23) #define _ZEND_TYPE_LIST_BIT (1u << 22) -#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT) +#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT|_ZEND_TYPE_LITERAL_NAME_BIT) /* For BC behaviour with iterable type */ #define _ZEND_TYPE_ITERABLE_BIT (1u << 21) /* Whether the type list is arena allocated */ @@ -171,6 +174,9 @@ typedef struct { #define ZEND_TYPE_HAS_NAME(t) \ ((((t).type_mask) & _ZEND_TYPE_NAME_BIT) != 0) +#define ZEND_TYPE_HAS_LITERAL_NAME(t) \ + ((((t).type_mask) & _ZEND_TYPE_LITERAL_NAME_BIT) != 0) + #define ZEND_TYPE_HAS_LIST(t) \ ((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0) @@ -289,11 +295,14 @@ typedef struct { #define ZEND_TYPE_INIT_CLASS(class_name, allow_null, extra_flags) \ ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags) +#define ZEND_TYPE_INIT_CLASS_MASK(class_name, type_mask) \ + ZEND_TYPE_INIT_PTR_MASK(class_name, _ZEND_TYPE_NAME_BIT | (type_mask)) + #define ZEND_TYPE_INIT_CLASS_CONST(class_name, allow_null, extra_flags) \ - ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_NAME_BIT, allow_null, extra_flags) + ZEND_TYPE_INIT_PTR(class_name, _ZEND_TYPE_LITERAL_NAME_BIT, allow_null, extra_flags) #define ZEND_TYPE_INIT_CLASS_CONST_MASK(class_name, type_mask) \ - ZEND_TYPE_INIT_PTR_MASK(class_name, _ZEND_TYPE_NAME_BIT | (type_mask)) + ZEND_TYPE_INIT_PTR_MASK(class_name, (_ZEND_TYPE_LITERAL_NAME_BIT | (type_mask))) typedef union _zend_value { zend_long lval; /* long value */ diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index b4ea2b27c9e9..ec82c727650b 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -884,11 +884,116 @@ static void le_throwing_resource_dtor(zend_resource *rsrc) zend_throw_exception(NULL, "Throwing resource destructor called", 0); } +static ZEND_METHOD(_ZendTestClass, takesUnionType) +{ + zend_object *obj; + ZEND_PARSE_PARAMETERS_START(1, 1); + Z_PARAM_OBJ(obj) + ZEND_PARSE_PARAMETERS_END(); + // we have to perform type-checking to avoid arginfo/zpp mismatch error + bool type_matches = ( + instanceof_function(obj->ce, zend_standard_class_def) + || + instanceof_function(obj->ce, zend_ce_iterator) + ); + if (!type_matches) { + zend_string *ty = zend_type_to_string(execute_data->func->internal_function.arg_info->type); + zend_argument_type_error(1, "must be of type %s, %s given", ty->val, obj->ce->name->val); + zend_string_release(ty); + RETURN_THROWS(); + } + + RETURN_NULL(); +} + +// Returns a newly allocated DNF type `Iterator|(Traversable&Countable)`. +// +// We need to generate it "manually" because gen_stubs.php does not support codegen for DNF types ATM. +static zend_type create_test_dnf_type(void) { + zend_string *class_Iterator = zend_string_init_interned("Iterator", sizeof("Iterator") - 1, true); + zend_alloc_ce_cache(class_Iterator); + zend_string *class_Traversable = ZSTR_KNOWN(ZEND_STR_TRAVERSABLE); + zend_string *class_Countable = zend_string_init_interned("Countable", sizeof("Countable") - 1, true); + zend_alloc_ce_cache(class_Countable); + // + zend_type_list *intersection_list = malloc(ZEND_TYPE_LIST_SIZE(2)); + intersection_list->num_types = 2; + intersection_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Traversable, 0, 0); + intersection_list->types[1] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Countable, 0, 0); + zend_type_list *union_list = malloc(ZEND_TYPE_LIST_SIZE(2)); + union_list->num_types = 2; + union_list->types[0] = (zend_type) ZEND_TYPE_INIT_CLASS(class_Iterator, 0, 0); + union_list->types[1] = (zend_type) ZEND_TYPE_INIT_INTERSECTION(intersection_list, 0); + return (zend_type) ZEND_TYPE_INIT_UNION(union_list, 0); +} + +static void register_ZendTestClass_dnf_property(zend_class_entry *ce) { + zend_string *prop_name = zend_string_init_interned("dnfProperty", sizeof("dnfProperty") - 1, true); + zval default_value; + ZVAL_UNDEF(&default_value); + zend_type type = create_test_dnf_type(); + zend_declare_typed_property(ce, prop_name, &default_value, ZEND_ACC_PUBLIC, NULL, type); +} + +// arg_info for `zend_test_internal_dnf_arguments` +// The types are upgraded to DNF types in `register_dynamic_function_entries()` +static zend_internal_arg_info arginfo_zend_test_internal_dnf_arguments[] = { + // first entry is a zend_internal_function_info (see zend_compile.h): {argument_count, return_type, unused} + {(const char*)(uintptr_t)(1), {0}, NULL}, + {"arg", {0}, NULL} +}; + +static ZEND_NAMED_FUNCTION(zend_test_internal_dnf_arguments) +{ + zend_object *obj; + ZEND_PARSE_PARAMETERS_START(1, 1); + Z_PARAM_OBJ(obj) + ZEND_PARSE_PARAMETERS_END(); + // we have to perform type-checking to avoid arginfo/zpp mismatch error + bool type_matches = ( + instanceof_function(obj->ce, zend_ce_iterator) + || ( + instanceof_function(obj->ce, zend_ce_traversable) + && instanceof_function(obj->ce, zend_ce_countable) + ) + ); + if (!type_matches) { + zend_string *ty = zend_type_to_string(arginfo_zend_test_internal_dnf_arguments[1].type); + zend_argument_type_error(1, "must be of type %s, %s given", ty->val, obj->ce->name->val); + zend_string_release(ty); + RETURN_THROWS(); + } + + RETURN_OBJ_COPY(obj); +} + +static const zend_function_entry dynamic_function_entries[] = { + { + .fname = "zend_test_internal_dnf_arguments", + .handler = zend_test_internal_dnf_arguments, + .arg_info = arginfo_zend_test_internal_dnf_arguments, + .num_args = 1, + .flags = 0, + }, + ZEND_FE_END, +}; + +static void register_dynamic_function_entries(int module_type) { + // return-type is at index 0 + arginfo_zend_test_internal_dnf_arguments[0].type = create_test_dnf_type(); + arginfo_zend_test_internal_dnf_arguments[1].type = create_test_dnf_type(); + // + zend_register_functions(NULL, dynamic_function_entries, NULL, module_type); +} + PHP_MINIT_FUNCTION(zend_test) { + register_dynamic_function_entries(type); + zend_test_interface = register_class__ZendTestInterface(); zend_test_class = register_class__ZendTestClass(zend_test_interface); + register_ZendTestClass_dnf_property(zend_test_class); zend_test_class->create_object = zend_test_class_new; zend_test_class->get_static_method = zend_test_class_static_method_get; diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index afaf79da2acd..312ad689b31d 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -53,6 +53,8 @@ public function returnsStatic(): static {} public function returnsThrowable(): Throwable {} static public function variadicTest(string|Iterator ...$elements) : static {} + + public function takesUnionType(stdclass|Iterator $arg): void {} } class _ZendTestChildClass extends _ZendTestClass diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index a56090fd565a..d5546638949b 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 87c580bffe8794d7597572c0d8571c7459420df8 */ + * Stub hash: b458993ee586284b1e33848313d9ddf61273604e */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -172,6 +172,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_variadicTes ZEND_ARG_VARIADIC_OBJ_TYPE_MASK(0, elements, Iterator, MAY_BE_STRING) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class__ZendTestClass_takesUnionType, 0, 1, IS_VOID, 0) + ZEND_ARG_OBJ_TYPE_MASK(0, arg, stdclass|Iterator, 0, NULL) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class__ZendTestChildClass_returnsThrowable, 0, 0, Exception, 0) ZEND_END_ARG_INFO() @@ -258,6 +262,7 @@ static ZEND_METHOD(_ZendTestClass, __toString); static ZEND_METHOD(_ZendTestClass, returnsStatic); static ZEND_METHOD(_ZendTestClass, returnsThrowable); static ZEND_METHOD(_ZendTestClass, variadicTest); +static ZEND_METHOD(_ZendTestClass, takesUnionType); static ZEND_METHOD(_ZendTestChildClass, returnsThrowable); static ZEND_METHOD(_ZendTestTrait, testMethod); static ZEND_METHOD(ZendTestParameterAttribute, __construct); @@ -340,6 +345,7 @@ static const zend_function_entry class__ZendTestClass_methods[] = { ZEND_ME(_ZendTestClass, returnsStatic, arginfo_class__ZendTestClass_returnsStatic, ZEND_ACC_PUBLIC) ZEND_ME(_ZendTestClass, returnsThrowable, arginfo_class__ZendTestClass_returnsThrowable, ZEND_ACC_PUBLIC) ZEND_ME(_ZendTestClass, variadicTest, arginfo_class__ZendTestClass_variadicTest, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(_ZendTestClass, takesUnionType, arginfo_class__ZendTestClass_takesUnionType, ZEND_ACC_PUBLIC) ZEND_FE_END }; diff --git a/ext/zend_test/tests/internal_dnf_arguments.phpt b/ext/zend_test/tests/internal_dnf_arguments.phpt new file mode 100644 index 000000000000..eb6f5fb514f0 --- /dev/null +++ b/ext/zend_test/tests/internal_dnf_arguments.phpt @@ -0,0 +1,34 @@ +--TEST-- +DNF types for internal functions +--EXTENSIONS-- +zend_test +spl +reflection +--FILE-- +getReturnType()); +$paramType = $rf->getParameters()[0]->getType(); +var_dump((string)$paramType); + +try { + zend_test_internal_dnf_arguments(new stdClass); +} catch (\Throwable $err) { + echo $err->getMessage(), "\n"; +} + +$obj = new \ArrayIterator([]); +$result = zend_test_internal_dnf_arguments($obj); +var_dump($result); + +?> +--EXPECT-- +string(32) "Iterator|(Traversable&Countable)" +string(32) "Iterator|(Traversable&Countable)" +zend_test_internal_dnf_arguments(): Argument #1 ($arg) must be of type Iterator|(Traversable&Countable), stdClass given +object(ArrayIterator)#5 (1) { + ["storage":"ArrayIterator":private]=> + array(0) { + } +}