Add array_is_list(array $array) function

This function tests if an array contains only sequential integer keys. While
list isn't an official type, this usage is consistent with the community usage
of "list" as an annotation type, cf.
https://psalm.dev/docs/annotating_code/type_syntax/array_types/#lists

Rebased and modified version of #4886

- Use .stub.php files
- Add opcache constant evaluation when argument is a constant
- Change from is_list(mixed $value) to array_is_list(array $array)

RFC: https://wiki.php.net/rfc/is_list

Co-Authored-By: Tyson Andre <tysonandre775@hotmail.com>
Co-Authored-By: Dusk <dusk@woofle.net>

Closes GH-6070
This commit is contained in:
Dusk 2019-11-03 18:51:49 -08:00 committed by Tyson Andre
parent 04db2c8735
commit 13c430b1db
7 changed files with 151 additions and 23 deletions

View file

@ -1188,6 +1188,33 @@ static zend_always_inline void *zend_hash_get_current_data_ptr_ex(HashTable *ht,
ZEND_HASH_FILL_FINISH(); \
} while (0)
/* Check if an array is a list */
static zend_always_inline zend_bool zend_array_is_list(zend_array *array)
{
zend_long expected_idx = 0;
zend_long num_idx;
zend_string* str_idx;
/* Empty arrays are lists */
if (zend_hash_num_elements(array) == 0) {
return 1;
}
/* Packed arrays are lists */
if (HT_IS_PACKED(array) && HT_IS_WITHOUT_HOLES(array)) {
return 1;
}
/* Check if the list could theoretically be repacked */
ZEND_HASH_FOREACH_KEY(array, num_idx, str_idx) {
if (str_idx != NULL || num_idx != expected_idx++) {
return 0;
}
} ZEND_HASH_FOREACH_END();
return 1;
}
static zend_always_inline zval *_zend_hash_append_ex(HashTable *ht, zend_string *key, zval *zv, bool interned)
{
uint32_t idx = ht->nNumUsed++;

View file

@ -36,29 +36,10 @@ static int php_json_escape_string(
static int php_json_determine_array_type(zval *val) /* {{{ */
{
int i;
HashTable *myht = Z_ARRVAL_P(val);
zend_array *myht = Z_ARRVAL_P(val);
i = myht ? zend_hash_num_elements(myht) : 0;
if (i > 0) {
zend_string *key;
zend_ulong index, idx;
if (HT_IS_PACKED(myht) && HT_IS_WITHOUT_HOLES(myht)) {
return PHP_JSON_OUTPUT_ARRAY;
}
idx = 0;
ZEND_HASH_FOREACH_KEY(myht, index, key) {
if (key) {
return PHP_JSON_OUTPUT_OBJECT;
} else {
if (index != idx) {
return PHP_JSON_OUTPUT_OBJECT;
}
}
idx++;
} ZEND_HASH_FOREACH_END();
if (myht) {
return zend_array_is_list(myht) ? PHP_JSON_OUTPUT_ARRAY : PHP_JSON_OUTPUT_OBJECT;
}
return PHP_JSON_OUTPUT_ARRAY;

View file

@ -788,6 +788,7 @@ static bool can_ct_eval_func_call(zend_string *name, uint32_t num_args, zval **a
|| zend_string_equals_literal(name, "array_diff")
|| zend_string_equals_literal(name, "array_diff_assoc")
|| zend_string_equals_literal(name, "array_diff_key")
|| zend_string_equals_literal(name, "array_is_list")
|| zend_string_equals_literal(name, "array_key_exists")
|| zend_string_equals_literal(name, "array_keys")
|| zend_string_equals_literal(name, "array_merge")

View file

@ -248,6 +248,8 @@ function array_chunk(array $array, int $length, bool $preserve_keys = false): ar
function array_combine(array $keys, array $values): array {}
function array_is_list(array $array): bool {}
/* base64.c */
function base64_encode(string $string): string {}

View file

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 4e471966d507762dd6fdd2fc4200c8430fac97f4 */
* Stub hash: 7540039937587f05584660bc1a1a8a80aa5ccbd1 */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0)
@ -360,6 +360,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_combine, 0, 2, IS_ARRAY, 0
ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_is_list, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_base64_encode, 0, 1, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
ZEND_END_ARG_INFO()
@ -2309,6 +2313,7 @@ ZEND_FUNCTION(array_map);
ZEND_FUNCTION(array_key_exists);
ZEND_FUNCTION(array_chunk);
ZEND_FUNCTION(array_combine);
ZEND_FUNCTION(array_is_list);
ZEND_FUNCTION(base64_encode);
ZEND_FUNCTION(base64_decode);
ZEND_FUNCTION(constant);
@ -2933,6 +2938,7 @@ static const zend_function_entry ext_functions[] = {
ZEND_FALIAS(key_exists, array_key_exists, arginfo_key_exists)
ZEND_FE(array_chunk, arginfo_array_chunk)
ZEND_FE(array_combine, arginfo_array_combine)
ZEND_FE(array_is_list, arginfo_array_is_list)
ZEND_FE(base64_encode, arginfo_base64_encode)
ZEND_FE(base64_decode, arginfo_base64_decode)
ZEND_FE(constant, arginfo_constant)

View file

@ -0,0 +1,98 @@
--TEST--
Test array_is_list() function
--FILE--
<?php
function test_is_list(string $desc, $val) : void {
try {
printf("%s: %s\n", $desc, json_encode(array_is_list($val)));
} catch (TypeError $e) {
printf("%s: threw %s\n", $desc, $e->getMessage());
}
}
test_is_list("empty", []);
test_is_list("one", [1]);
test_is_list("two", [1,2]);
test_is_list("three", [1,2,3]);
test_is_list("four", [1,2,3,4]);
test_is_list("ten", range(0, 10));
test_is_list("null", null);
test_is_list("int", 123);
test_is_list("float", 1.23);
test_is_list("string", "string");
test_is_list("object", new stdClass());
test_is_list("true", true);
test_is_list("false", false);
test_is_list("string key", ["a" => 1]);
test_is_list("mixed keys", [0 => 0, "a" => 1]);
test_is_list("ordered keys", [0 => 0, 1 => 1]);
test_is_list("shuffled keys", [1 => 0, 0 => 1]);
test_is_list("skipped keys", [0 => 0, 2 => 2]);
$arr = [1, 2, 3];
unset($arr[0]);
test_is_list("unset first", $arr);
$arr = [1, 2, 3];
unset($arr[1]);
test_is_list("unset middle", $arr);
$arr = [1, 2, 3];
unset($arr[2]);
test_is_list("unset end", $arr);
$arr = [1, "a" => "a", 2];
unset($arr["a"]);
test_is_list("unset string key", $arr);
$arr = [1 => 1, 0 => 0];
unset($arr[1]);
test_is_list("unset into order", $arr);
$arr = ["a" => 1];
unset($arr["a"]);
test_is_list("unset to empty", $arr);
$arr = [1, 2, 3];
$arr[] = 4;
test_is_list("append implicit", $arr);
$arr = [1, 2, 3];
$arr[3] = 4;
test_is_list("append explicit", $arr);
$arr = [1, 2, 3];
$arr[4] = 5;
test_is_list("append with gap", $arr);
--EXPECT--
empty: true
one: true
two: true
three: true
four: true
ten: true
null: threw array_is_list(): Argument #1 ($array) must be of type array, null given
int: threw array_is_list(): Argument #1 ($array) must be of type array, int given
float: threw array_is_list(): Argument #1 ($array) must be of type array, float given
string: threw array_is_list(): Argument #1 ($array) must be of type array, string given
object: threw array_is_list(): Argument #1 ($array) must be of type array, stdClass given
true: threw array_is_list(): Argument #1 ($array) must be of type array, bool given
false: threw array_is_list(): Argument #1 ($array) must be of type array, bool given
string key: false
mixed keys: false
ordered keys: true
shuffled keys: false
skipped keys: false
unset first: false
unset middle: false
unset end: true
unset string key: true
unset into order: true
unset to empty: true
append implicit: true
append explicit: true
append with gap: false

View file

@ -321,6 +321,19 @@ PHP_FUNCTION(is_array)
}
/* }}} */
/* {{{ Returns true if $array is an array whose keys are all numeric, sequential, and start at 0 */
PHP_FUNCTION(array_is_list)
{
HashTable *array;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_HT(array)
ZEND_PARSE_PARAMETERS_END();
RETURN_BOOL(zend_array_is_list(array));
}
/* }}} */
/* {{{ Returns true if variable is an object
Warning: This function is special-cased by zend_compile.c and so is usually bypassed */
PHP_FUNCTION(is_object)