Adds support for DNF types in internal functions and properties (#11969)

Note that this does not add support for items generated by gen_stubs,
only for items registered dynamically via the Zend API.

Closes GH-10120
This commit is contained in:
ju1ius 2023-08-19 01:11:06 +02:00 committed by GitHub
parent 4ff93f779c
commit 7f1c3bf09b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 205 additions and 24 deletions

View file

@ -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

View file

@ -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);

View file

@ -6438,7 +6438,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;
}

View file

@ -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 */

View file

@ -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;

View file

@ -53,6 +53,8 @@ namespace {
public function returnsThrowable(): Throwable {}
static public function variadicTest(string|Iterator ...$elements) : static {}
public function takesUnionType(stdclass|Iterator $arg): void {}
}
class _ZendTestChildClass extends _ZendTestClass

View file

@ -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
};

View file

@ -0,0 +1,34 @@
--TEST--
DNF types for internal functions
--EXTENSIONS--
zend_test
spl
reflection
--FILE--
<?php
$rf = new \ReflectionFunction('zend_test_internal_dnf_arguments');
var_dump((string)$rf->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) {
}
}