]> granicus.if.org Git - php/commitdiff
Add array_is_list(array $array) function
authorDusk <dusk@woofle.net>
Mon, 4 Nov 2019 02:51:49 +0000 (18:51 -0800)
committerTyson Andre <tysonandre775@hotmail.com>
Wed, 20 Jan 2021 23:53:48 +0000 (18:53 -0500)
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

Zend/zend_hash.h
ext/json/json_encoder.c
ext/opcache/Optimizer/sccp.c
ext/standard/basic_functions.stub.php
ext/standard/basic_functions_arginfo.h
ext/standard/tests/general_functions/array_is_list.phpt [new file with mode: 0644]
ext/standard/type.c

index e339345a32eb5a58bb0d543ba95845502b00613d..64a9220573dcae50d0cc7b855e65cccb4923b8a4 100644 (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++;
index 92e4a109339575350e693d0260d17d48ed7a4b94..f60893634998bc8bf3bf1406344d8ad555912d98 100644 (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);
-
-       i = myht ? zend_hash_num_elements(myht) : 0;
-       if (i > 0) {
-               zend_string *key;
-               zend_ulong index, idx;
+       zend_array *myht = Z_ARRVAL_P(val);
 
-               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;
index 7f9bdbdbb568e5c7b19ca0638d04554f8c2443c8..bc9dc38680d4c728871d19f4e77011b419e497e3 100644 (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")
index b6597d262e2999017c9cc12193b722c1327f462c..99d0d3c63db7a9801d26d8b0b7695d7f3dea5c24 100755 (executable)
@@ -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 {}
index e416fc5e272bb2e1abd3a25a6bb0e97809f094d9..5eafa59b50bd7667f8dfecc06c49632aaa78034c 100644 (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)
diff --git a/ext/standard/tests/general_functions/array_is_list.phpt b/ext/standard/tests/general_functions/array_is_list.phpt
new file mode 100644 (file)
index 0000000..1cc2886
--- /dev/null
@@ -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
\ No newline at end of file
index afe0e7afc29e13f283ab28319de0ca150bb4bc25..1036dd7d06615361926c03ed6e22f4850a0a42fd 100644 (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)