Skip to content

Commit

Permalink
Add array_is_list(array $array) function
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
duskwuff and TysonAndre committed Jan 6, 2021
1 parent 555bd2f commit 9b131d5
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 23 deletions.
27 changes: 27 additions & 0 deletions Zend/zend_hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand Down
25 changes: 3 additions & 22 deletions ext/json/json_encoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions ext/opcache/Optimizer/sccp.c
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,7 @@ static zend_bool can_ct_eval_func_call(zend_string *name, uint32_t num_args, zva
|| 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")
Expand Down
2 changes: 2 additions & 0 deletions ext/standard/basic_functions.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
8 changes: 7 additions & 1 deletion ext/standard/basic_functions_arginfo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 21e54280829776de72313b96e38ad2aee60bd0ee */
* Stub hash: a4779a303018370a7a222d80cd69cb37d6a52db9 */

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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
98 changes: 98 additions & 0 deletions ext/standard/tests/general_functions/array_is_list.phpt
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions ext/standard/type.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 9b131d5

Please sign in to comment.