From 68b3b49a479943fb74e5098f19cb5c23a8239243 Mon Sep 17 00:00:00 2001 From: Dusk Date: Sun, 3 Nov 2019 18:51:49 -0800 Subject: [PATCH] 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 --- ext/standard/basic_functions.c | 5 + ext/standard/php_type.h | 1 + .../tests/general_functions/is_list.phpt | 94 +++++++++++++++++++ ext/standard/type.c | 37 ++++++++ 4 files changed, 137 insertions(+) create mode 100644 ext/standard/tests/general_functions/is_list.phpt diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 6135ace457596..0873cb9316786 100755 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -982,6 +982,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO(arginfo_is_array, _IS_BOOL, 0) ZEND_ARG_INFO(0, var) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO(arginfo_is_list, _IS_BOOL, 0) + ZEND_ARG_INFO(0, var) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO(arginfo_is_object, _IS_BOOL, 0) ZEND_ARG_INFO(0, var) ZEND_END_ARG_INFO() @@ -1539,6 +1543,7 @@ static const zend_function_entry basic_functions[] = { /* {{{ */ PHP_FE(is_numeric, arginfo_is_numeric) PHP_FE(is_string, arginfo_is_string) PHP_FE(is_array, arginfo_is_array) + PHP_FE(is_list, arginfo_is_list) PHP_FE(is_object, arginfo_is_object) PHP_FE(is_scalar, arginfo_is_scalar) PHP_FE(is_callable, arginfo_is_callable) diff --git a/ext/standard/php_type.h b/ext/standard/php_type.h index be056201dbfb9..612d0de408b2d 100644 --- a/ext/standard/php_type.h +++ b/ext/standard/php_type.h @@ -31,6 +31,7 @@ PHP_FUNCTION(is_float); PHP_FUNCTION(is_numeric); PHP_FUNCTION(is_string); PHP_FUNCTION(is_array); +PHP_FUNCTION(is_list); PHP_FUNCTION(is_object); PHP_FUNCTION(is_scalar); PHP_FUNCTION(is_callable); diff --git a/ext/standard/tests/general_functions/is_list.phpt b/ext/standard/tests/general_functions/is_list.phpt new file mode 100644 index 0000000000000..db20df1584dfb --- /dev/null +++ b/ext/standard/tests/general_functions/is_list.phpt @@ -0,0 +1,94 @@ +--TEST-- +Test is_list() function +--FILE-- + 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: false +int: false +float: false +string: false +object: false +true: false +false: false +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 diff --git a/ext/standard/type.c b/ext/standard/type.c index af847bf352ce1..c0b083137c827 100644 --- a/ext/standard/type.c +++ b/ext/standard/type.c @@ -289,6 +289,43 @@ PHP_FUNCTION(is_array) } /* }}} */ +/* {{{ proto bool is_list(mixed var) + Returns true if variable is an array whose keys are all numeric, sequential, + and start at 0 */ +PHP_FUNCTION(is_list) +{ + zval *arg; + zend_array *arrval; + zend_ulong num_idx, expected_idx = 0; + zend_string *str_idx; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(arg) + ZEND_PARSE_PARAMETERS_END(); + + if (Z_TYPE_P(arg) != IS_ARRAY) + RETURN_FALSE; + + arrval = Z_ARRVAL_P(arg); + + /* Empty arrays are lists */ + if (zend_hash_num_elements(arrval) == 0) + RETURN_TRUE; + + /* Packed arrays are lists */ + if (HT_IS_PACKED(arrval) && HT_IS_WITHOUT_HOLES(arrval)) + RETURN_TRUE; + + /* Check if the list could theoretically be repacked */ + ZEND_HASH_FOREACH_KEY(arrval, num_idx, str_idx) { + if (str_idx != NULL || num_idx != expected_idx++) + RETURN_FALSE; + } ZEND_HASH_FOREACH_END(); + + RETURN_TRUE; +} +/* }}} */ + /* {{{ proto bool is_object(mixed var) Returns true if variable is an object Warning: This function is special-cased by zend_compile.c and so is usually bypassed */