Skip to content

Commit 13c430b

Browse files
duskwuffTysonAndre
authored andcommitted
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 <[email protected]> Co-Authored-By: Dusk <[email protected]> Closes GH-6070
1 parent 04db2c8 commit 13c430b

File tree

7 files changed

+151
-23
lines changed

7 files changed

+151
-23
lines changed

Zend/zend_hash.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,33 @@ static zend_always_inline void *zend_hash_get_current_data_ptr_ex(HashTable *ht,
11881188
ZEND_HASH_FILL_FINISH(); \
11891189
} while (0)
11901190

1191+
/* Check if an array is a list */
1192+
static zend_always_inline zend_bool zend_array_is_list(zend_array *array)
1193+
{
1194+
zend_long expected_idx = 0;
1195+
zend_long num_idx;
1196+
zend_string* str_idx;
1197+
/* Empty arrays are lists */
1198+
if (zend_hash_num_elements(array) == 0) {
1199+
return 1;
1200+
}
1201+
1202+
/* Packed arrays are lists */
1203+
if (HT_IS_PACKED(array) && HT_IS_WITHOUT_HOLES(array)) {
1204+
return 1;
1205+
}
1206+
1207+
/* Check if the list could theoretically be repacked */
1208+
ZEND_HASH_FOREACH_KEY(array, num_idx, str_idx) {
1209+
if (str_idx != NULL || num_idx != expected_idx++) {
1210+
return 0;
1211+
}
1212+
} ZEND_HASH_FOREACH_END();
1213+
1214+
return 1;
1215+
}
1216+
1217+
11911218
static zend_always_inline zval *_zend_hash_append_ex(HashTable *ht, zend_string *key, zval *zv, bool interned)
11921219
{
11931220
uint32_t idx = ht->nNumUsed++;

ext/json/json_encoder.c

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,10 @@ static int php_json_escape_string(
3636

3737
static int php_json_determine_array_type(zval *val) /* {{{ */
3838
{
39-
int i;
40-
HashTable *myht = Z_ARRVAL_P(val);
41-
42-
i = myht ? zend_hash_num_elements(myht) : 0;
43-
if (i > 0) {
44-
zend_string *key;
45-
zend_ulong index, idx;
39+
zend_array *myht = Z_ARRVAL_P(val);
4640

47-
if (HT_IS_PACKED(myht) && HT_IS_WITHOUT_HOLES(myht)) {
48-
return PHP_JSON_OUTPUT_ARRAY;
49-
}
50-
51-
idx = 0;
52-
ZEND_HASH_FOREACH_KEY(myht, index, key) {
53-
if (key) {
54-
return PHP_JSON_OUTPUT_OBJECT;
55-
} else {
56-
if (index != idx) {
57-
return PHP_JSON_OUTPUT_OBJECT;
58-
}
59-
}
60-
idx++;
61-
} ZEND_HASH_FOREACH_END();
41+
if (myht) {
42+
return zend_array_is_list(myht) ? PHP_JSON_OUTPUT_ARRAY : PHP_JSON_OUTPUT_OBJECT;
6243
}
6344

6445
return PHP_JSON_OUTPUT_ARRAY;

ext/opcache/Optimizer/sccp.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,7 @@ static bool can_ct_eval_func_call(zend_string *name, uint32_t num_args, zval **a
788788
|| zend_string_equals_literal(name, "array_diff")
789789
|| zend_string_equals_literal(name, "array_diff_assoc")
790790
|| zend_string_equals_literal(name, "array_diff_key")
791+
|| zend_string_equals_literal(name, "array_is_list")
791792
|| zend_string_equals_literal(name, "array_key_exists")
792793
|| zend_string_equals_literal(name, "array_keys")
793794
|| zend_string_equals_literal(name, "array_merge")

ext/standard/basic_functions.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ function array_chunk(array $array, int $length, bool $preserve_keys = false): ar
248248

249249
function array_combine(array $keys, array $values): array {}
250250

251+
function array_is_list(array $array): bool {}
252+
251253
/* base64.c */
252254

253255
function base64_encode(string $string): string {}

ext/standard/basic_functions_arginfo.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 4e471966d507762dd6fdd2fc4200c8430fac97f4 */
2+
* Stub hash: 7540039937587f05584660bc1a1a8a80aa5ccbd1 */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0)
55
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
360360
ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0)
361361
ZEND_END_ARG_INFO()
362362

363+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_is_list, 0, 1, _IS_BOOL, 0)
364+
ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0)
365+
ZEND_END_ARG_INFO()
366+
363367
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_base64_encode, 0, 1, IS_STRING, 0)
364368
ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
365369
ZEND_END_ARG_INFO()
@@ -2309,6 +2313,7 @@ ZEND_FUNCTION(array_map);
23092313
ZEND_FUNCTION(array_key_exists);
23102314
ZEND_FUNCTION(array_chunk);
23112315
ZEND_FUNCTION(array_combine);
2316+
ZEND_FUNCTION(array_is_list);
23122317
ZEND_FUNCTION(base64_encode);
23132318
ZEND_FUNCTION(base64_decode);
23142319
ZEND_FUNCTION(constant);
@@ -2933,6 +2938,7 @@ static const zend_function_entry ext_functions[] = {
29332938
ZEND_FALIAS(key_exists, array_key_exists, arginfo_key_exists)
29342939
ZEND_FE(array_chunk, arginfo_array_chunk)
29352940
ZEND_FE(array_combine, arginfo_array_combine)
2941+
ZEND_FE(array_is_list, arginfo_array_is_list)
29362942
ZEND_FE(base64_encode, arginfo_base64_encode)
29372943
ZEND_FE(base64_decode, arginfo_base64_decode)
29382944
ZEND_FE(constant, arginfo_constant)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
--TEST--
2+
Test array_is_list() function
3+
--FILE--
4+
<?php
5+
6+
function test_is_list(string $desc, $val) : void {
7+
try {
8+
printf("%s: %s\n", $desc, json_encode(array_is_list($val)));
9+
} catch (TypeError $e) {
10+
printf("%s: threw %s\n", $desc, $e->getMessage());
11+
}
12+
}
13+
14+
test_is_list("empty", []);
15+
test_is_list("one", [1]);
16+
test_is_list("two", [1,2]);
17+
test_is_list("three", [1,2,3]);
18+
test_is_list("four", [1,2,3,4]);
19+
test_is_list("ten", range(0, 10));
20+
21+
test_is_list("null", null);
22+
test_is_list("int", 123);
23+
test_is_list("float", 1.23);
24+
test_is_list("string", "string");
25+
test_is_list("object", new stdClass());
26+
test_is_list("true", true);
27+
test_is_list("false", false);
28+
29+
test_is_list("string key", ["a" => 1]);
30+
test_is_list("mixed keys", [0 => 0, "a" => 1]);
31+
test_is_list("ordered keys", [0 => 0, 1 => 1]);
32+
test_is_list("shuffled keys", [1 => 0, 0 => 1]);
33+
test_is_list("skipped keys", [0 => 0, 2 => 2]);
34+
35+
$arr = [1, 2, 3];
36+
unset($arr[0]);
37+
test_is_list("unset first", $arr);
38+
39+
$arr = [1, 2, 3];
40+
unset($arr[1]);
41+
test_is_list("unset middle", $arr);
42+
43+
$arr = [1, 2, 3];
44+
unset($arr[2]);
45+
test_is_list("unset end", $arr);
46+
47+
$arr = [1, "a" => "a", 2];
48+
unset($arr["a"]);
49+
test_is_list("unset string key", $arr);
50+
51+
$arr = [1 => 1, 0 => 0];
52+
unset($arr[1]);
53+
test_is_list("unset into order", $arr);
54+
55+
$arr = ["a" => 1];
56+
unset($arr["a"]);
57+
test_is_list("unset to empty", $arr);
58+
59+
$arr = [1, 2, 3];
60+
$arr[] = 4;
61+
test_is_list("append implicit", $arr);
62+
63+
$arr = [1, 2, 3];
64+
$arr[3] = 4;
65+
test_is_list("append explicit", $arr);
66+
67+
$arr = [1, 2, 3];
68+
$arr[4] = 5;
69+
test_is_list("append with gap", $arr);
70+
71+
--EXPECT--
72+
empty: true
73+
one: true
74+
two: true
75+
three: true
76+
four: true
77+
ten: true
78+
null: threw array_is_list(): Argument #1 ($array) must be of type array, null given
79+
int: threw array_is_list(): Argument #1 ($array) must be of type array, int given
80+
float: threw array_is_list(): Argument #1 ($array) must be of type array, float given
81+
string: threw array_is_list(): Argument #1 ($array) must be of type array, string given
82+
object: threw array_is_list(): Argument #1 ($array) must be of type array, stdClass given
83+
true: threw array_is_list(): Argument #1 ($array) must be of type array, bool given
84+
false: threw array_is_list(): Argument #1 ($array) must be of type array, bool given
85+
string key: false
86+
mixed keys: false
87+
ordered keys: true
88+
shuffled keys: false
89+
skipped keys: false
90+
unset first: false
91+
unset middle: false
92+
unset end: true
93+
unset string key: true
94+
unset into order: true
95+
unset to empty: true
96+
append implicit: true
97+
append explicit: true
98+
append with gap: false

ext/standard/type.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,19 @@ PHP_FUNCTION(is_array)
321321
}
322322
/* }}} */
323323

324+
/* {{{ Returns true if $array is an array whose keys are all numeric, sequential, and start at 0 */
325+
PHP_FUNCTION(array_is_list)
326+
{
327+
HashTable *array;
328+
329+
ZEND_PARSE_PARAMETERS_START(1, 1)
330+
Z_PARAM_ARRAY_HT(array)
331+
ZEND_PARSE_PARAMETERS_END();
332+
333+
RETURN_BOOL(zend_array_is_list(array));
334+
}
335+
/* }}} */
336+
324337
/* {{{ Returns true if variable is an object
325338
Warning: This function is special-cased by zend_compile.c and so is usually bypassed */
326339
PHP_FUNCTION(is_object)

0 commit comments

Comments
 (0)