Skip to content

Commit 293dc5f

Browse files
duskwuffTysonAndre
andcommitted
Add is_list function
This function tests if an array contains only sequential integer keys. While this 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 version of #4886 - Use .stub.php files - Add opcache constant evaluation when argument is a constant Co-Authored-By: Tyson Andre <[email protected]> Co-Authored-By: Dusk <[email protected]>
1 parent 70c22de commit 293dc5f

File tree

7 files changed

+152
-23
lines changed

7 files changed

+152
-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 & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,10 @@ static int php_json_escape_string(
3737
static int php_json_determine_array_type(zval *val) /* {{{ */
3838
{
3939
int i;
40-
HashTable *myht = Z_ARRVAL_P(val);
40+
zend_array *myht = Z_ARRVAL_P(val);
4141

42-
i = myht ? zend_hash_num_elements(myht) : 0;
43-
if (i > 0) {
44-
zend_string *key;
45-
zend_ulong index, idx;
46-
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();
42+
if (myht) {
43+
return zend_array_is_list(myht) ? PHP_JSON_OUTPUT_ARRAY : PHP_JSON_OUTPUT_OBJECT;
6244
}
6345

6446
return PHP_JSON_OUTPUT_ARRAY;

ext/opcache/Optimizer/sccp.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ static zend_bool can_ct_eval_func_call(zend_string *name, uint32_t num_args, zva
799799
|| zend_string_equals_literal(name, "base64_encode")
800800
|| zend_string_equals_literal(name, "imagetypes")
801801
|| zend_string_equals_literal(name, "in_array")
802+
|| zend_string_equals_literal(name, "is_list")
802803
|| zend_string_equals_literal(name, "ltrim")
803804
|| zend_string_equals_literal(name, "php_sapi_name")
804805
|| zend_string_equals_literal(name, "php_uname")
@@ -853,7 +854,6 @@ static zend_bool can_ct_eval_func_call(zend_string *name, uint32_t num_args, zva
853854
return false;
854855
}
855856
} ZEND_HASH_FOREACH_END();
856-
return true;
857857
}
858858
return false;
859859
}

ext/standard/basic_functions.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,8 @@ function is_string(mixed $value): bool {}
14081408

14091409
function is_array(mixed $value): bool {}
14101410

1411+
function is_list(mixed $value): bool {}
1412+
14111413
function is_object(mixed $value): bool {}
14121414

14131415
function is_scalar(mixed $value): bool {}

ext/standard/basic_functions_arginfo.h

Lines changed: 5 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: 4edb7cad23ccb051dbe267b3979e98892607c98f */
2+
* Stub hash: e7c27569f3028091cfa1faa7b262c9e98f5da8a9 */
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)
@@ -2074,6 +2074,8 @@ ZEND_END_ARG_INFO()
20742074

20752075
#define arginfo_is_array arginfo_boolval
20762076

2077+
#define arginfo_is_list arginfo_boolval
2078+
20772079
#define arginfo_is_object arginfo_boolval
20782080

20792081
#define arginfo_is_scalar arginfo_boolval
@@ -2781,6 +2783,7 @@ ZEND_FUNCTION(is_float);
27812783
ZEND_FUNCTION(is_numeric);
27822784
ZEND_FUNCTION(is_string);
27832785
ZEND_FUNCTION(is_array);
2786+
ZEND_FUNCTION(is_list);
27842787
ZEND_FUNCTION(is_object);
27852788
ZEND_FUNCTION(is_scalar);
27862789
ZEND_FUNCTION(is_callable);
@@ -3432,6 +3435,7 @@ static const zend_function_entry ext_functions[] = {
34323435
ZEND_FE(is_numeric, arginfo_is_numeric)
34333436
ZEND_FE(is_string, arginfo_is_string)
34343437
ZEND_FE(is_array, arginfo_is_array)
3438+
ZEND_FE(is_list, arginfo_is_list)
34353439
ZEND_FE(is_object, arginfo_is_object)
34363440
ZEND_FE(is_scalar, arginfo_is_scalar)
34373441
ZEND_FE(is_callable, arginfo_is_callable)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
--TEST--
2+
Test is_list() function
3+
--FILE--
4+
<?php
5+
6+
function test_is_list(string $desc, $val) : void {
7+
printf("%s: %s\n", $desc, is_list($val) ? "true" : "false");
8+
}
9+
10+
test_is_list("empty", []);
11+
test_is_list("one", [1]);
12+
test_is_list("two", [1,2]);
13+
test_is_list("three", [1,2,3]);
14+
test_is_list("four", [1,2,3,4]);
15+
test_is_list("ten", range(0, 10));
16+
17+
test_is_list("null", null);
18+
test_is_list("int", 123);
19+
test_is_list("float", 1.23);
20+
test_is_list("string", "string");
21+
test_is_list("object", new stdclass);
22+
test_is_list("true", true);
23+
test_is_list("false", false);
24+
25+
test_is_list("string key", ["a" => 1]);
26+
test_is_list("mixed keys", [0 => 0, "a" => 1]);
27+
test_is_list("ordered keys", [0 => 0, 1 => 1]);
28+
test_is_list("shuffled keys", [1 => 0, 0 => 1]);
29+
test_is_list("skipped keys", [0 => 0, 2 => 2]);
30+
31+
$arr = [1, 2, 3];
32+
unset($arr[0]);
33+
test_is_list("unset first", $arr);
34+
35+
$arr = [1, 2, 3];
36+
unset($arr[1]);
37+
test_is_list("unset middle", $arr);
38+
39+
$arr = [1, 2, 3];
40+
unset($arr[2]);
41+
test_is_list("unset end", $arr);
42+
43+
$arr = [1, "a" => "a", 2];
44+
unset($arr["a"]);
45+
test_is_list("unset string key", $arr);
46+
47+
$arr = [1 => 1, 0 => 0];
48+
unset($arr[1]);
49+
test_is_list("unset into order", $arr);
50+
51+
$arr = ["a" => 1];
52+
unset($arr["a"]);
53+
test_is_list("unset to empty", $arr);
54+
55+
$arr = [1, 2, 3];
56+
$arr[] = 4;
57+
test_is_list("append implicit", $arr);
58+
59+
$arr = [1, 2, 3];
60+
$arr[3] = 4;
61+
test_is_list("append explicit", $arr);
62+
63+
$arr = [1, 2, 3];
64+
$arr[4] = 5;
65+
test_is_list("append with gap", $arr);
66+
67+
--EXPECT--
68+
empty: true
69+
one: true
70+
two: true
71+
three: true
72+
four: true
73+
ten: true
74+
null: false
75+
int: false
76+
float: false
77+
string: false
78+
object: false
79+
true: false
80+
false: false
81+
string key: false
82+
mixed keys: false
83+
ordered keys: true
84+
shuffled keys: false
85+
skipped keys: false
86+
unset first: false
87+
unset middle: false
88+
unset end: true
89+
unset string key: true
90+
unset into order: true
91+
unset to empty: true
92+
append implicit: true
93+
append explicit: true
94+
append with gap: false

ext/standard/type.c

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

324+
/* {{{ Returns true if variable is an array whose keys are all numeric, sequential, and start at 0 */
325+
PHP_FUNCTION(is_list)
326+
{
327+
zval *arg;
328+
zend_array *arrval;
329+
330+
ZEND_PARSE_PARAMETERS_START(1, 1)
331+
Z_PARAM_ZVAL(arg)
332+
ZEND_PARSE_PARAMETERS_END();
333+
334+
if (Z_TYPE_P(arg) != IS_ARRAY)
335+
RETURN_FALSE;
336+
337+
arrval = Z_ARRVAL_P(arg);
338+
339+
/* Empty arrays are lists */
340+
RETURN_BOOL(zend_array_is_list(arrval));
341+
}
342+
/* }}} */
343+
324344
/* {{{ Returns true if variable is an object
325345
Warning: This function is special-cased by zend_compile.c and so is usually bypassed */
326346
PHP_FUNCTION(is_object)

0 commit comments

Comments
 (0)