Convert iterable into an internal alias for Traversable|array (#7309)

This does a compile time transformation of ``iterable`` into ``Traversable|array`` which simplifies some of the LSP variance handling.

The arginfo generation script from stubs is updated to produce a union type when it encounters the type ``iterable``
Extension functions which do not regenerate the arginfo, or write them manually are still supported by mimicking the compile time transformation while registering the function.

Type Reflection is preserved for single ``iterable`` (and ``?iterable``) to produce a ReflectionNamedType with name ``iterable``, however usage of ``iterable`` in union types will be converted to ``array|Traversable``
This commit is contained in:
George Peter Banyard 2022-06-07 13:35:34 +01:00 committed by GitHub
parent 94183d3e8b
commit b40ae80804
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 341 additions and 198 deletions

View File

@ -2114,9 +2114,6 @@ static uint32_t zend_convert_type_declaration_mask(uint32_t type_mask) {
if (type_mask & MAY_BE_CALLABLE) {
result_mask |= MAY_BE_STRING|MAY_BE_OBJECT|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF;
}
if (type_mask & MAY_BE_ITERABLE) {
result_mask |= MAY_BE_OBJECT|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF;
}
if (type_mask & MAY_BE_STATIC) {
result_mask |= MAY_BE_OBJECT;
}

View File

@ -26,6 +26,10 @@ function test6() : object|callable {
yield 6;
}
function test7() : iterable {
yield 7;
}
var_dump(
test1(),
test2(),
@ -33,6 +37,7 @@ var_dump(
test4(),
test5(),
test6(),
test7(),
);
?>
--EXPECTF--
@ -48,3 +53,5 @@ object(Generator)#%d (%d) {
}
object(Generator)#%d (%d) {
}
object(Generator)#%d (%d) {
}

View File

@ -7,4 +7,4 @@ function foo(): iterable&Iterator {}
?>
--EXPECTF--
Fatal error: Type iterable cannot be part of an intersection type in %s on line %d
Fatal error: Type Traversable|array cannot be part of an intersection type in %s on line %d

View File

@ -15,4 +15,4 @@ class Test2 extends Test {
?>
--EXPECTF--
Fatal error: Declaration of Test2::method(): X&Y must be compatible with Test::method(): iterable in %s on line %d
Fatal error: Declaration of Test2::method(): X&Y must be compatible with Test::method(): Traversable|array in %s on line %d

View File

@ -45,4 +45,4 @@ object(ArrayIterator)#1 (1) {
int(3)
}
}
test(): Argument #1 ($iterable) must be of type iterable, int given, called in %s on line %d
test(): Argument #1 ($iterable) must be of type Traversable|array, int given, called in %s on line %d

View File

@ -17,4 +17,4 @@ function baz(iterable $iterable = 1) {
?>
--EXPECTF--
Fatal error: Cannot use int as default value for parameter $iterable of type iterable in %s on line %d
Fatal error: Cannot use int as default value for parameter $iterable of type Traversable|array in %s on line %d

View File

@ -29,4 +29,4 @@ array(0) {
}
object(Generator)#2 (0) {
}
baz(): Return value must be of type iterable, int returned
baz(): Return value must be of type Traversable|array, int returned

View File

@ -21,4 +21,4 @@ class Bar extends Foo {
?>
--EXPECTF--
Fatal error: Declaration of Bar::testScalar(iterable $iterable) must be compatible with Foo::testScalar(int $int) in %s on line %d
Fatal error: Declaration of Bar::testScalar(Traversable|array $iterable) must be compatible with Foo::testScalar(int $int) in %s on line %d

View File

@ -29,4 +29,4 @@ class TestScalar extends Test {
?>
--EXPECTF--
Fatal error: Declaration of TestScalar::method(): int must be compatible with Test::method(): iterable in %s on line %d
Fatal error: Declaration of TestScalar::method(): int must be compatible with Test::method(): Traversable|array in %s on line %d

View File

@ -0,0 +1,46 @@
--TEST--
Test "or null"/"or be null" in type-checking errors for userland functions with iterable
--FILE--
<?php
// This should test every branch in zend_execute.c's `zend_verify_arg_type`, `zend_verify_return_type` and `zend_verify_missing_return_type` functions which produces an "or null"/"or be null" part in its error message
function iterableF(?iterable $param) {}
try {
iterableF(1);
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}
function returnIterable(): ?iterable {
return 1;
}
try {
returnIterable();
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}
function returnMissingIterable(): ?iterable {
}
try {
returnMissingIterable();
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}
?>
--EXPECTF--
TypeError: iterableF(): Argument #1 ($param) must be of type Traversable|array|null, int given, called in %s on line %d and defined in %s:%d
Stack trace:
#0 %s(%d): iterableF(1)
#1 {main}
TypeError: returnIterable(): Return value must be of type Traversable|array|null, int returned in %s:%d
Stack trace:
#0 %s(%d): returnIterable()
#1 {main}
TypeError: returnMissingIterable(): Return value must be of type Traversable|array|null, none returned in %s:%d
Stack trace:
#0 %s(%d): returnMissingIterable()
#1 {main}

View File

@ -8,4 +8,4 @@ function test(): iterable|Traversable {
?>
--EXPECTF--
Fatal error: Type Traversable|iterable contains both iterable and Traversable, which is redundant in %s on line %d
Fatal error: Duplicate type Traversable is redundant in %s on line %d

View File

@ -8,4 +8,4 @@ function test(): iterable|Traversable|ArrayAccess {
?>
--EXPECTF--
Fatal error: Type Traversable|ArrayAccess|iterable contains both iterable and Traversable, which is redundant in %s on line %d
Fatal error: Duplicate type Traversable is redundant in %s on line %d

View File

@ -8,4 +8,4 @@ function test(): iterable|array {
?>
--EXPECTF--
Fatal error: Type iterable|array contains both iterable and array, which is redundant in %s on line %d
Fatal error: Duplicate type array is redundant in %s on line %d

View File

@ -57,14 +57,6 @@ try {
echo $e, PHP_EOL;
}
function iterableF(?iterable $param) {}
try {
iterableF(1);
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}
function intF(?int $param) {}
try {
@ -143,16 +135,6 @@ try {
echo $e, PHP_EOL;
}
function returnIterable(): ?iterable {
return 1;
}
try {
returnIterable();
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}
function returnInt(): ?int {
return new \StdClass;
}
@ -199,15 +181,6 @@ try {
echo $e, PHP_EOL;
}
function returnMissingIterable(): ?iterable {
}
try {
returnMissingIterable();
} catch (\TypeError $e) {
echo $e, PHP_EOL;
}
function returnMissingInt(): ?int {
}
@ -221,97 +194,85 @@ try {
--EXPECTF--
TypeError: unloadedClass(): Argument #1 ($param) must be of type ?I\Dont\Exist, stdClass given, called in %s:%d
Stack trace:
#0 %s(8): unloadedClass(Object(stdClass))
#0 %s(%d): unloadedClass(Object(stdClass))
#1 {main}
TypeError: loadedClass(): Argument #1 ($param) must be of type ?RealClass, stdClass given, called in %s:%d
Stack trace:
#0 %s(20): loadedClass(Object(stdClass))
#0 %s(%d): loadedClass(Object(stdClass))
#1 {main}
TypeError: loadedInterface(): Argument #1 ($param) must be of type ?RealInterface, stdClass given, called in %s:%d
Stack trace:
#0 %s(26): loadedInterface(Object(stdClass))
#0 %s(%d): loadedInterface(Object(stdClass))
#1 {main}
TypeError: unloadedClass(): Argument #1 ($param) must be of type ?I\Dont\Exist, int given, called in %s:%d
Stack trace:
#0 %s(32): unloadedClass(1)
#0 %s(%d): unloadedClass(1)
#1 {main}
TypeError: loadedClass(): Argument #1 ($param) must be of type ?RealClass, int given, called in %s:%d
Stack trace:
#0 %s(38): loadedClass(1)
#0 %s(%d): loadedClass(1)
#1 {main}
TypeError: loadedInterface(): Argument #1 ($param) must be of type ?RealInterface, int given, called in %s:%d
Stack trace:
#0 %s(44): loadedInterface(1)
#0 %s(%d): loadedInterface(1)
#1 {main}
TypeError: callableF(): Argument #1 ($param) must be of type ?callable, int given, called in %s:%d
Stack trace:
#0 %s(52): callableF(1)
#1 {main}
TypeError: iterableF(): Argument #1 ($param) must be of type ?iterable, int given, called in %s:%d
Stack trace:
#0 %s(60): iterableF(1)
#0 %s(%d): callableF(1)
#1 {main}
TypeError: intF(): Argument #1 ($param) must be of type ?int, stdClass given, called in %s:%d
Stack trace:
#0 %s(68): intF(Object(stdClass))
#0 %s(%d): intF(Object(stdClass))
#1 {main}
TypeError: returnUnloadedClass(): Return value must be of type ?I\Dont\Exist, stdClass returned in %s:%d
Stack trace:
#0 %s(78): returnUnloadedClass()
#0 %s(%d): returnUnloadedClass()
#1 {main}
TypeError: returnLoadedClass(): Return value must be of type ?RealClass, stdClass returned in %s:%d
Stack trace:
#0 %s(88): returnLoadedClass()
#0 %s(%d): returnLoadedClass()
#1 {main}
TypeError: returnLoadedInterface(): Return value must be of type ?RealInterface, stdClass returned in %s:%d
Stack trace:
#0 %s(98): returnLoadedInterface()
#0 %s(%d): returnLoadedInterface()
#1 {main}
TypeError: returnUnloadedClassScalar(): Return value must be of type ?I\Dont\Exist, int returned in %s:%d
Stack trace:
#0 %s(108): returnUnloadedClassScalar()
#0 %s(%d): returnUnloadedClassScalar()
#1 {main}
TypeError: returnLoadedClassScalar(): Return value must be of type ?RealClass, int returned in %s:%d
Stack trace:
#0 %s(118): returnLoadedClassScalar()
#0 %s(%d): returnLoadedClassScalar()
#1 {main}
TypeError: returnLoadedInterfaceScalar(): Return value must be of type ?RealInterface, int returned in %s:%d
Stack trace:
#0 %s(128): returnLoadedInterfaceScalar()
#0 %s(%d): returnLoadedInterfaceScalar()
#1 {main}
TypeError: returnCallable(): Return value must be of type ?callable, int returned in %s:%d
Stack trace:
#0 %s(138): returnCallable()
#1 {main}
TypeError: returnIterable(): Return value must be of type ?iterable, int returned in %s:%d
Stack trace:
#0 %s(148): returnIterable()
#0 %s(%d): returnCallable()
#1 {main}
TypeError: returnInt(): Return value must be of type ?int, stdClass returned in %s:%d
Stack trace:
#0 %s(158): returnInt()
#0 %s(%d): returnInt()
#1 {main}
TypeError: returnMissingUnloadedClass(): Return value must be of type ?I\Dont\Exist, none returned in %s:%d
Stack trace:
#0 %s(167): returnMissingUnloadedClass()
#0 %s(%d): returnMissingUnloadedClass()
#1 {main}
TypeError: returnMissingLoadedClass(): Return value must be of type ?RealClass, none returned in %s:%d
Stack trace:
#0 %s(176): returnMissingLoadedClass()
#0 %s(%d): returnMissingLoadedClass()
#1 {main}
TypeError: returnMissingLoadedInterface(): Return value must be of type ?RealInterface, none returned in %s:%d
Stack trace:
#0 %s(185): returnMissingLoadedInterface()
#0 %s(%d): returnMissingLoadedInterface()
#1 {main}
TypeError: returnMissingCallable(): Return value must be of type ?callable, none returned in %s:%d
Stack trace:
#0 %s(194): returnMissingCallable()
#1 {main}
TypeError: returnMissingIterable(): Return value must be of type ?iterable, none returned in %s:%d
Stack trace:
#0 %s(203): returnMissingIterable()
#0 %s(%d): returnMissingCallable()
#1 {main}
TypeError: returnMissingInt(): Return value must be of type ?int, none returned in %s:%d
Stack trace:
#0 %s(212): returnMissingInt()
#0 %s(%d): returnMissingInt()
#1 {main}

View File

@ -2805,6 +2805,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
}
}
/* Rebuild arginfos if parameter/property types and/or a return type are used */
if (reg_function->common.arg_info &&
(reg_function->common.fn_flags & (ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_HAS_TYPE_HINTS))) {
/* convert "const char*" class type names into "zend_string*" */
@ -2856,6 +2857,15 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
}
}
}
if (ZEND_TYPE_IS_ITERABLE_FALLBACK(new_arg_info[i].type)) {
/* Warning generated an extension load warning which is emitted for every test
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));
new_arg_info[i].type = legacy_iterable;
}
}
}

View File

@ -1227,9 +1227,6 @@ zend_string *zend_type_to_string_resolved(zend_type type, zend_class_entry *scop
if (type_mask & MAY_BE_CALLABLE) {
str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_CALLABLE), /* is_intersection */ false);
}
if (type_mask & MAY_BE_ITERABLE) {
str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_ITERABLE), /* is_intersection */ false);
}
if (type_mask & MAY_BE_OBJECT) {
str = add_type_string(str, ZSTR_KNOWN(ZEND_STR_OBJECT), /* is_intersection */ false);
}
@ -1289,7 +1286,7 @@ static void zend_mark_function_as_generator(void) /* {{{ */
if (CG(active_op_array)->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
zend_type return_type = CG(active_op_array)->arg_info[-1].type;
bool valid_type = (ZEND_TYPE_FULL_MASK(return_type) & (MAY_BE_ITERABLE | MAY_BE_OBJECT)) != 0;
bool valid_type = (ZEND_TYPE_FULL_MASK(return_type) & MAY_BE_OBJECT) != 0;
if (!valid_type) {
zend_type *single_type;
ZEND_TYPE_FOREACH(return_type, single_type) {
@ -6134,6 +6131,7 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
zend_error_noreturn(E_COMPILE_ERROR,
"Cannot use \"static\" when no class scope is active");
}
return (zend_type) ZEND_TYPE_INIT_CODE(ast->attr, 0, 0);
} else {
zend_string *class_name = zend_ast_get_str(ast);
@ -6145,6 +6143,15 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
"Type declaration '%s' must be unqualified",
ZSTR_VAL(zend_string_tolower(class_name)));
}
/* 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),
(MAY_BE_ARRAY|_ZEND_TYPE_ITERABLE_BIT));
return iterable;
}
return (zend_type) ZEND_TYPE_INIT_CODE(type_code, 0, 0);
} else {
const char *correct_name;
@ -6184,19 +6191,6 @@ static zend_type zend_compile_single_typename(zend_ast *ast)
}
}
static bool zend_type_contains_traversable(zend_type type) {
zend_type *single_type;
ZEND_TYPE_FOREACH(type, single_type) {
if (ZEND_TYPE_HAS_NAME(*single_type)
&& zend_string_equals_literal_ci(ZEND_TYPE_NAME(*single_type), "Traversable")) {
return 1;
}
} ZEND_TYPE_FOREACH_END();
return 0;
}
// TODO: Ideally we'd canonicalize "iterable" into "array|Traversable" and essentially
// treat it as a built-in type alias.
static zend_type zend_compile_typename(
zend_ast *ast, bool force_allow_null) /* {{{ */
{
@ -6290,6 +6284,14 @@ static zend_type zend_compile_typename(
zend_ast *type_ast = list->child[i];
zend_type single_type = zend_compile_single_typename(type_ast);
/* An intersection of union types cannot exist so invalidate it
* Currently only can happen with iterable getting canonicalized to Traversable|array */
if (ZEND_TYPE_IS_ITERABLE_FALLBACK(single_type)) {
zend_string *standard_type_str = zend_type_to_string(single_type);
zend_error_noreturn(E_COMPILE_ERROR,
"Type %s cannot be part of an intersection type", ZSTR_VAL(standard_type_str));
zend_string_release_ex(standard_type_str, false);
}
/* An intersection of standard types cannot exist so invalidate it */
if (ZEND_TYPE_IS_ONLY_MASK(single_type)) {
zend_string *standard_type_str = zend_type_to_string(single_type);
@ -6329,18 +6331,6 @@ static zend_type zend_compile_typename(
}
uint32_t type_mask = ZEND_TYPE_PURE_MASK(type);
if ((type_mask & (MAY_BE_ARRAY|MAY_BE_ITERABLE)) == (MAY_BE_ARRAY|MAY_BE_ITERABLE)) {
zend_string *type_str = zend_type_to_string(type);
zend_error_noreturn(E_COMPILE_ERROR,
"Type %s contains both iterable and array, which is redundant", ZSTR_VAL(type_str));
}
if ((type_mask & MAY_BE_ITERABLE) && zend_type_contains_traversable(type)) {
zend_string *type_str = zend_type_to_string(type);
zend_error_noreturn(E_COMPILE_ERROR,
"Type %s contains both iterable and Traversable, which is redundant",
ZSTR_VAL(type_str));
}
if (type_mask == MAY_BE_ANY && is_marked_nullable) {
zend_error_noreturn(E_COMPILE_ERROR, "Type mixed cannot be marked as nullable since mixed already includes null");
@ -6387,9 +6377,6 @@ static bool zend_is_valid_default_value(zend_type type, zval *value)
convert_to_double(value);
return 1;
}
if ((ZEND_TYPE_FULL_MASK(type) & MAY_BE_ITERABLE) && Z_TYPE_P(value) == IS_ARRAY) {
return 1;
}
return 0;
}

View File

@ -909,9 +909,6 @@ static zend_always_inline bool i_zend_check_property_type(zend_property_info *in
uint32_t type_mask = ZEND_TYPE_FULL_MASK(info->type);
ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC)));
if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(property)) {
return 1;
}
return zend_verify_scalar_type_hint(type_mask, property, strict, 0);
}
@ -1048,9 +1045,6 @@ static zend_always_inline bool zend_check_type_slow(
if ((type_mask & MAY_BE_CALLABLE) && zend_is_callable(arg, 0, NULL)) {
return 1;
}
if ((type_mask & MAY_BE_ITERABLE) && zend_is_iterable(arg)) {
return 1;
}
if ((type_mask & MAY_BE_STATIC) && zend_value_instanceof_static(arg)) {
return 1;
}
@ -2929,7 +2923,7 @@ static zend_always_inline bool check_type_array_assignable(zend_type type) {
if (!ZEND_TYPE_IS_SET(type)) {
return 1;
}
return (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) != 0;
return (ZEND_TYPE_FULL_MASK(type) & MAY_BE_ARRAY) != 0;
}
/* Checks whether an array can be assigned to the reference. Throws error if not assignable. */
@ -3378,9 +3372,6 @@ static zend_always_inline int i_zend_verify_type_assignable_zval(
type_mask = ZEND_TYPE_FULL_MASK(type);
ZEND_ASSERT(!(type_mask & (MAY_BE_CALLABLE|MAY_BE_STATIC)));
if (type_mask & MAY_BE_ITERABLE) {
return zend_is_iterable(zv);
}
/* SSTH Exception: IS_LONG may be accepted as IS_DOUBLE (converted) */
if (strict) {

View File

@ -328,21 +328,6 @@ static bool unlinked_instanceof(zend_class_entry *ce1, zend_class_entry *ce2) {
return 0;
}
static bool zend_type_contains_traversable(zend_type type) {
zend_type *single_type;
if (ZEND_TYPE_FULL_MASK(type) & MAY_BE_OBJECT) {
return 1;
}
ZEND_TYPE_FOREACH(type, single_type) {
if (ZEND_TYPE_HAS_NAME(*single_type)
&& zend_string_equals_literal_ci(ZEND_TYPE_NAME(*single_type), "Traversable")) {
return 1;
}
} ZEND_TYPE_FOREACH_END();
return 0;
}
static bool zend_type_permits_self(
zend_type type, zend_class_entry *scope, zend_class_entry *self) {
if (ZEND_TYPE_FULL_MASK(type) & MAY_BE_OBJECT) {
@ -473,15 +458,6 @@ static inheritance_status zend_is_class_subtype_of_type(
return INHERITANCE_SUCCESS;
}
}
if (ZEND_TYPE_FULL_MASK(proto_type) & MAY_BE_ITERABLE) {
if (!fe_ce) fe_ce = lookup_class(fe_scope, fe_class_name);
if (!fe_ce) {
have_unresolved = 1;
} else if (unlinked_instanceof(fe_ce, zend_ce_traversable)) {
track_class_dependency(fe_ce, fe_class_name);
return INHERITANCE_SUCCESS;
}
}
zend_type *single_type;
@ -567,18 +543,6 @@ static inheritance_status zend_perform_covariant_type_check(
uint32_t proto_type_mask = ZEND_TYPE_PURE_MASK(proto_type);
uint32_t added_types = fe_type_mask & ~proto_type_mask;
if (added_types) {
// TODO: Make "iterable" an alias of "array|Traversable" instead,
// so these special cases will be handled automatically.
if ((added_types & MAY_BE_ITERABLE)
&& (proto_type_mask & MAY_BE_ARRAY)
&& zend_type_contains_traversable(proto_type)) {
/* Replacing array|Traversable with iterable is okay */
added_types &= ~MAY_BE_ITERABLE;
}
if ((added_types & MAY_BE_ARRAY) && (proto_type_mask & MAY_BE_ITERABLE)) {
/* Replacing iterable with array is okay */
added_types &= ~MAY_BE_ARRAY;
}
if ((added_types & MAY_BE_STATIC)
&& zend_type_permits_self(proto_type, proto_scope, fe_scope)) {
/* Replacing type that accepts self with static is okay */
@ -605,8 +569,7 @@ static inheritance_status zend_perform_covariant_type_check(
* We still perform a class lookup for forward-compatibility reasons,
* as we may have named types in the future that are not classes
* (such as typedefs). */
if (proto_type_mask & (MAY_BE_OBJECT|MAY_BE_ITERABLE)) {
bool any_class = (proto_type_mask & MAY_BE_OBJECT) != 0;
if (proto_type_mask & MAY_BE_OBJECT) {
ZEND_TYPE_FOREACH(fe_type, single_type) {
zend_string *fe_class_name = get_class_from_type(fe_scope, *single_type);
if (!fe_class_name) {
@ -614,10 +577,8 @@ static inheritance_status zend_perform_covariant_type_check(
}
zend_class_entry *fe_ce = lookup_class(fe_scope, fe_class_name);
if (fe_ce) {
if (any_class || unlinked_instanceof(fe_ce, zend_ce_traversable)) {
track_class_dependency(fe_ce, fe_class_name);
return INHERITANCE_SUCCESS;
}
track_class_dependency(fe_ce, fe_class_name);
return INHERITANCE_SUCCESS;
} else {
have_unresolved = true;
}

View File

@ -580,6 +580,7 @@ EMPTY_SWITCH_DEFAULT_CASE()
_(ZEND_STR_FALSE, "false") \
_(ZEND_STR_NULL_LOWERCASE, "null") \
_(ZEND_STR_MIXED, "mixed") \
_(ZEND_STR_TRAVERSABLE, "Traversable") \
_(ZEND_STR_SLEEP, "__sleep") \
_(ZEND_STR_WAKEUP, "__wakeup") \
_(ZEND_STR_CASES, "cases") \

View File

@ -38,7 +38,6 @@
/* These are used in zend_type, but not for type inference.
* They are allowed to overlap with types used during inference. */
#define MAY_BE_CALLABLE (1 << IS_CALLABLE)
#define MAY_BE_ITERABLE (1 << IS_ITERABLE)
#define MAY_BE_VOID (1 << IS_VOID)
#define MAY_BE_NEVER (1 << IS_NEVER)
#define MAY_BE_STATIC (1 << IS_STATIC)

View File

@ -144,7 +144,8 @@ typedef struct {
#define _ZEND_TYPE_NAME_BIT (1u << 24)
#define _ZEND_TYPE_LIST_BIT (1u << 22)
#define _ZEND_TYPE_KIND_MASK (_ZEND_TYPE_LIST_BIT|_ZEND_TYPE_NAME_BIT)
/* TODO: bit 21 is not used */
/* For BC behaviour with iterable type */
#define _ZEND_TYPE_ITERABLE_BIT (1u << 21)
/* Whether the type list is arena allocated */
#define _ZEND_TYPE_ARENA_BIT (1u << 20)
/* Whether the type list is an intersection type */
@ -170,6 +171,9 @@ typedef struct {
#define ZEND_TYPE_HAS_LIST(t) \
((((t).type_mask) & _ZEND_TYPE_LIST_BIT) != 0)
#define ZEND_TYPE_IS_ITERABLE_FALLBACK(t) \
((((t).type_mask) & _ZEND_TYPE_ITERABLE_BIT) != 0)
#define ZEND_TYPE_IS_INTERSECTION(t) \
((((t).type_mask) & _ZEND_TYPE_INTERSECTION_BIT) != 0)
@ -263,7 +267,7 @@ typedef struct {
{ NULL, (_type_mask) }
#define ZEND_TYPE_INIT_CODE(code, allow_null, extra_flags) \
ZEND_TYPE_INIT_MASK(((code) == _IS_BOOL ? MAY_BE_BOOL : ((code) == IS_MIXED ? MAY_BE_ANY : (1 << (code)))) \
ZEND_TYPE_INIT_MASK(((code) == _IS_BOOL ? MAY_BE_BOOL : ( (code) == IS_ITERABLE ? _ZEND_TYPE_ITERABLE_BIT : ((code) == IS_MIXED ? MAY_BE_ANY : (1 << (code))))) \
| ((allow_null) ? _ZEND_TYPE_NULLABLE_BIT : 0) | (extra_flags))
#define ZEND_TYPE_INIT_PTR(ptr, type_kind, allow_null, extra_flags) \

View File

@ -219,7 +219,6 @@ class SimpleType {
case "float":
case "string":
case "callable":
case "iterable":
case "object":
case "resource":
case "mixed":
@ -231,6 +230,8 @@ class SimpleType {
return ArrayType::createGenericArray();
case "self":
throw new Exception('The exact class name must be used instead of "self"');
case "iterable":
throw new Exception('This should not happen');
}
$matches = [];
@ -369,8 +370,6 @@ class SimpleType {
return "IS_VOID";
case "callable":
return "IS_CALLABLE";
case "iterable":
return "IS_ITERABLE";
case "mixed":
return "IS_MIXED";
case "static":
@ -408,8 +407,6 @@ class SimpleType {
return "MAY_BE_OBJECT";
case "callable":
return "MAY_BE_CALLABLE";
case "iterable":
return "MAY_BE_ITERABLE";
case "mixed":
return "MAY_BE_ANY";
case "void":
@ -509,18 +506,32 @@ class Type {
public static function fromNode(Node $node): Type {
if ($node instanceof Node\UnionType) {
return new Type(array_map(['SimpleType', 'fromNode'], $node->types));
$nestedTypeObjects = array_map(['Type', 'fromNode'], $node->types);
$types = [];
foreach ($nestedTypeObjects as $typeObject) {
array_push($types, ...$typeObject->types);
}
return new Type($types);
}
if ($node instanceof Node\NullableType) {
return new Type(
[
SimpleType::fromNode($node->type),
...Type::fromNode($node->type)->types,
SimpleType::null(),
]
);
}
if ($node instanceof Node\Identifier && $node->toLowerString() === "iterable") {
return new Type(
[
SimpleType::fromString("Traversable"),
ArrayType::createGenericArray(),
]
);
}
return new Type([SimpleType::fromNode($node)]);
}

View File

@ -12232,7 +12232,7 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
uint32_t flags = opline->extended_value & ZEND_FETCH_OBJ_FLAGS;
if (flags == ZEND_FETCH_DIM_WRITE) {
if ((ZEND_TYPE_FULL_MASK(prop_info->type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) == 0) {
if ((ZEND_TYPE_FULL_MASK(prop_info->type) & MAY_BE_ARRAY) == 0) {
if (!type_loaded) {
type_loaded = 1;
| MEM_ACCESS_32_WITH_UOFFSET ldr, REG2w, FCARG1x, (prop_info->offset + offsetof(zval,u1.type_info)), TMP1

View File

@ -1949,7 +1949,7 @@ static zend_always_inline bool check_type_array_assignable(zend_type type) {
if (!ZEND_TYPE_IS_SET(type)) {
return 1;
}
return (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) != 0;
return (ZEND_TYPE_FULL_MASK(type) & MAY_BE_ARRAY) != 0;
}
static zend_property_info *zend_object_fetch_property_type_info(
@ -2094,7 +2094,7 @@ static void ZEND_FASTCALL zend_jit_check_array_promotion(zval *val, zend_propert
if ((Z_TYPE_P(val) <= IS_FALSE
|| (Z_ISREF_P(val) && Z_TYPE_P(Z_REFVAL_P(val)) <= IS_FALSE))
&& ZEND_TYPE_IS_SET(prop->type)
&& (ZEND_TYPE_FULL_MASK(prop->type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) == 0) {
&& (ZEND_TYPE_FULL_MASK(prop->type) & MAY_BE_ARRAY) == 0) {
zend_string *type_str = zend_type_to_string(prop->type);
zend_type_error(
"Cannot auto-initialize an array inside property %s::$%s of type %s",

View File

@ -12962,7 +12962,7 @@ static int zend_jit_fetch_obj(dasm_State **Dst,
uint32_t flags = opline->extended_value & ZEND_FETCH_OBJ_FLAGS;
if (flags == ZEND_FETCH_DIM_WRITE) {
if ((ZEND_TYPE_FULL_MASK(prop_info->type) & (MAY_BE_ITERABLE|MAY_BE_ARRAY)) == 0) {
if ((ZEND_TYPE_FULL_MASK(prop_info->type) & MAY_BE_ARRAY) == 0) {
if (!type_loaded) {
type_loaded = 1;
| mov edx, dword [FCARG1a + prop_info->offset + 8]

View File

@ -12,7 +12,7 @@ test(new stdClass);
?>
--EXPECTF--
Fatal error: Uncaught TypeError: test(): Return value must be of type iterable, stdClass returned in %s:%d
Fatal error: Uncaught TypeError: test(): Return value must be of type Traversable|array, stdClass returned in %s:%d
Stack trace:
#0 %s(%d): test(Object(stdClass))
#1 {main}

View File

@ -1330,6 +1330,7 @@ typedef enum {
} reflection_type_kind;
/* For backwards compatibility reasons, we need to return T|null style unions
* and transformation from iterable to Traversable|array
* as a ReflectionNamedType. Here we determine what counts as a union type and
* what doesn't. */
static reflection_type_kind get_type_kind(zend_type type) {
@ -1344,6 +1345,10 @@ static reflection_type_kind get_type_kind(zend_type type) {
}
if (ZEND_TYPE_IS_COMPLEX(type)) {
/* BC support for 'iterable' type */
if (UNEXPECTED(ZEND_TYPE_IS_ITERABLE_FALLBACK(type))) {
return NAMED_TYPE;
}
if (type_mask_without_null != 0) {
return UNION_TYPE;
}
@ -2726,6 +2731,11 @@ ZEND_METHOD(ReflectionParameter, isArray)
}
GET_REFLECTION_OBJECT_PTR(param);
/* BC For iterable */
if (ZEND_TYPE_IS_ITERABLE_FALLBACK(param->arg_info->type)) {
RETURN_FALSE;
}
type_mask = ZEND_TYPE_PURE_MASK_WITHOUT_NULL(param->arg_info->type);
RETVAL_BOOL(type_mask == MAY_BE_ARRAY);
}
@ -3006,9 +3016,21 @@ ZEND_METHOD(ReflectionType, allowsNull)
}
/* }}} */
/* For BC with iterable for named types */
static zend_string *zend_named_reflection_type_to_string(zend_type type) {
if (ZEND_TYPE_IS_ITERABLE_FALLBACK(type)) {
zend_string *iterable = ZSTR_KNOWN(ZEND_STR_ITERABLE);
if (ZEND_TYPE_FULL_MASK(type) & MAY_BE_NULL) {
return zend_string_concat2("?", strlen("?"), ZSTR_VAL(iterable), ZSTR_LEN(iterable));
}
return iterable;
}
return zend_type_to_string(type);
}
static zend_string *zend_type_to_string_without_null(zend_type type) {
ZEND_TYPE_FULL_MASK(type) &= ~MAY_BE_NULL;
return zend_type_to_string(type);
return zend_named_reflection_type_to_string(type);
}
/* {{{ Return the text of the type hint */
@ -3022,7 +3044,7 @@ ZEND_METHOD(ReflectionType, __toString)
}
GET_REFLECTION_OBJECT_PTR(param);
RETURN_STR(zend_type_to_string(param->type));
RETURN_STR(zend_named_reflection_type_to_string(param->type));
}
/* }}} */
@ -3040,7 +3062,7 @@ ZEND_METHOD(ReflectionNamedType, getName)
if (param->legacy_behavior) {
RETURN_STR(zend_type_to_string_without_null(param->type));
}
RETURN_STR(zend_type_to_string(param->type));
RETURN_STR(zend_named_reflection_type_to_string(param->type));
}
/* }}} */
@ -3055,6 +3077,10 @@ ZEND_METHOD(ReflectionNamedType, isBuiltin)
}
GET_REFLECTION_OBJECT_PTR(param);
if (ZEND_TYPE_IS_ITERABLE_FALLBACK(param->type)) {
RETURN_TRUE;
}
/* Treat "static" as a class type for the purposes of reflection. */
RETVAL_BOOL(ZEND_TYPE_IS_ONLY_MASK(param->type)
&& !(ZEND_TYPE_FULL_MASK(param->type) & MAY_BE_STATIC));
@ -3063,6 +3089,11 @@ ZEND_METHOD(ReflectionNamedType, isBuiltin)
static void append_type(zval *return_value, zend_type type) {
zval reflection_type;
/* Drop iterable BC bit for type list */
if (ZEND_TYPE_IS_ITERABLE_FALLBACK(type)) {
ZEND_TYPE_FULL_MASK(type) &= ~_ZEND_TYPE_ITERABLE_BIT;
}
reflection_type_factory(type, &reflection_type, 0);
zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &reflection_type);
}
@ -3103,9 +3134,6 @@ ZEND_METHOD(ReflectionUnionType, getTypes)
if (type_mask & MAY_BE_CALLABLE) {
append_type_mask(return_value, MAY_BE_CALLABLE);
}
if (type_mask & MAY_BE_ITERABLE) {
append_type_mask(return_value, MAY_BE_ITERABLE);
}
if (type_mask & MAY_BE_OBJECT) {
append_type_mask(return_value, MAY_BE_OBJECT);
}

View File

@ -80,7 +80,6 @@ class PropTypeTest {
public int $int;
public string $string;
public array $arr;
public iterable $iterable;
public stdClass $std;
public OtherThing $other;
public $mixed;
@ -216,7 +215,6 @@ string(4) "Test"
public int $int;
public string $string;
public array $arr;
public iterable $iterable;
public stdClass $std;
public OtherThing $other;
public $mixed;

View File

@ -11,7 +11,6 @@ $functions = [
function(): bool {},
function(): array {},
function(): callable {},
function(): iterable {},
function(): null {},
function(): false {},
function(): StdClass {}
@ -31,7 +30,6 @@ string(6) "string"
string(4) "bool"
string(5) "array"
string(8) "callable"
string(8) "iterable"
string(4) "null"
string(5) "false"
string(8) "StdClass"

View File

@ -1,10 +0,0 @@
--TEST--
Bug #72661 (ReflectionType::__toString crashes with iterable)
--FILE--
<?php
function test(iterable $arg) { }
var_dump((string)(new ReflectionParameter("test", 0))->getType());
?>
--EXPECT--
string(8) "iterable"

View File

@ -0,0 +1,98 @@
--TEST--
iterable Type in Reflection
--FILE--
<?php
$function = function(iterable $arg): iterable {};
$paramType = (new ReflectionParameter($function, 0))->getType();
var_dump($paramType::class);
var_dump($paramType);
var_dump($paramType->getName());
var_dump((string) $paramType);
var_dump($paramType->isBuiltin());
$reflectionFunc = new ReflectionFunction($function);
$returnType = $reflectionFunc->getReturnType();
var_dump($returnType::class);
var_dump($returnType);
var_dump($returnType->getName());
var_dump((string) $returnType);
var_dump($returnType->isBuiltin());
class PropIterableTypeTest {
public iterable $iterable;
public ?iterable $nullableIterable;
public array $control;
public ?array $nullableControl;
}
$reflector = new ReflectionClass(PropIterableTypeTest::class);
[$property, $nullable, $control, $nullableControl] = $reflector->getProperties();
$iterableType = $property->getType();
var_dump($iterableType::class);
var_dump($iterableType);
var_dump($iterableType->getName());
var_dump((string) $iterableType);
var_dump($iterableType->isBuiltin());
$nullableIterableType = $nullable->getType();
var_dump($nullableIterableType::class);
var_dump($nullableIterableType);
var_dump($nullableIterableType->getName());
var_dump((string) $nullableIterableType);
var_dump($nullableIterableType->isBuiltin());
$controlType = $control->getType();
var_dump($controlType::class);
var_dump($controlType);
var_dump($controlType->getName());
var_dump((string) $controlType);
var_dump($controlType->isBuiltin());
$nullableControlType = $nullableControl->getType();
var_dump($nullableControlType::class);
var_dump($nullableControlType);
var_dump($nullableControlType->getName());
var_dump((string) $nullableControlType);
var_dump($nullableControlType->isBuiltin());
?>
--EXPECTF--
string(19) "ReflectionNamedType"
object(ReflectionNamedType)#%d (0) {
}
string(8) "iterable"
string(8) "iterable"
bool(true)
string(19) "ReflectionNamedType"
object(ReflectionNamedType)#%d (0) {
}
string(8) "iterable"
string(8) "iterable"
bool(true)
string(19) "ReflectionNamedType"
object(ReflectionNamedType)#%d (0) {
}
string(8) "iterable"
string(8) "iterable"
bool(true)
string(19) "ReflectionNamedType"
object(ReflectionNamedType)#%d (0) {
}
string(8) "iterable"
string(9) "?iterable"
bool(true)
string(19) "ReflectionNamedType"
object(ReflectionNamedType)#%d (0) {
}
string(5) "array"
string(5) "array"
bool(true)
string(19) "ReflectionNamedType"
object(ReflectionNamedType)#%d (0) {
}
string(5) "array"
string(6) "?array"
bool(true)

View File

@ -75,13 +75,16 @@ Allows null: true
Name: null
String: null
Allows Null: true
Type X|iterable|bool:
Type X|Traversable|array|bool:
Allows null: false
Name: X
String: X
Allows Null: false
Name: iterable
String: iterable
Name: Traversable
String: Traversable
Allows Null: false
Name: array
String: array
Allows Null: false
Name: bool
String: bool

View File

@ -314,6 +314,29 @@ static ZEND_FUNCTION(zend_iterable)
ZEND_PARSE_PARAMETERS_END();
}
static ZEND_FUNCTION(zend_iterable_legacy)
{
zval *arg1, *arg2;
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_ITERABLE(arg1)
Z_PARAM_OPTIONAL
Z_PARAM_ITERABLE_OR_NULL(arg2)
ZEND_PARSE_PARAMETERS_END();
RETURN_COPY(arg1);
}
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_iterable_legacy, 0, 1, IS_ITERABLE, 0)
ZEND_ARG_TYPE_INFO(0, arg1, IS_ITERABLE, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, arg2, IS_ITERABLE, 1, "null")
ZEND_END_ARG_INFO()
static const zend_function_entry ext_function_legacy[] = {
ZEND_FE(zend_iterable_legacy, arginfo_zend_iterable_legacy)
ZEND_FE_END
};
/* Call a method on a class or object using zend_call_method() */
static ZEND_FUNCTION(zend_call_method)
{
@ -628,6 +651,8 @@ PHP_MINIT_FUNCTION(zend_test)
zend_test_string_enum = register_class_ZendTestStringEnum();
zend_test_int_enum = register_class_ZendTestIntEnum();
zend_register_functions(NULL, ext_function_legacy, NULL, EG(current_module)->type);
// Loading via dl() not supported with the observer API
if (type != MODULE_TEMPORARY) {
REGISTER_INI_ENTRIES();

View File

@ -53,8 +53,8 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_zend_string_or_stdclass_or_n
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_iterable, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, arg1, IS_ITERABLE, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, arg2, IS_ITERABLE, 1, "null")
ZEND_ARG_OBJ_TYPE_MASK(0, arg1, Traversable, MAY_BE_ARRAY, NULL)
ZEND_ARG_OBJ_TYPE_MASK(0, arg2, Traversable, MAY_BE_ARRAY|MAY_BE_NULL, "null")
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_weakmap_attach, 0, 2, _IS_BOOL, 0)

View File

@ -0,0 +1,28 @@
--TEST--
Test that legacy IS_ITERABLE arg info type generates a notice
--EXTENSIONS--
zend_test
--FILE--
<?php
function gen() {
yield 1;
};
var_dump(zend_iterable_legacy([], null));
var_dump(zend_iterable_legacy([], []));
var_dump(zend_iterable_legacy(gen(), null));
var_dump(zend_iterable_legacy(gen(), gen()));
?>
==DONE==
--EXPECT--
array(0) {
}
array(0) {
}
object(Generator)#1 (0) {
}
object(Generator)#1 (0) {
}
==DONE==

View File

@ -51,4 +51,4 @@ array(1) {
[%s]=>
object(stdClass)#2 (0) {
}
}
}