From 5a2482bce7c5ed800715d3b9c5581dceffb4d5fe 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 Rebased version of #4886 - Use .stub.php files - Add opcache constant evaluation when argument is a constant Co-Authored-By: Tyson Andre Co-Authored-By: Dusk --- Zend/zend_hash.h | 27 ++++++ ext/json/json_encoder.c | 25 +---- ext/opcache/Optimizer/sccp.c | 2 +- ext/standard/basic_functions.stub.php | 2 + ext/standard/basic_functions_arginfo.h | 6 +- .../tests/general_functions/is_list.phpt | 94 +++++++++++++++++++ ext/standard/type.c | 19 ++++ 7 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 ext/standard/tests/general_functions/is_list.phpt diff --git a/Zend/zend_hash.h b/Zend/zend_hash.h index ced21a2ca1557..f744226930a66 100644 --- a/Zend/zend_hash.h +++ b/Zend/zend_hash.h @@ -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++; diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c index 92e4a10933957..f60893634998b 100644 --- a/ext/json/json_encoder.c +++ b/ext/json/json_encoder.c @@ -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; diff --git a/ext/opcache/Optimizer/sccp.c b/ext/opcache/Optimizer/sccp.c index b1979b68a8ab1..c53f48e128286 100644 --- a/ext/opcache/Optimizer/sccp.c +++ b/ext/opcache/Optimizer/sccp.c @@ -799,6 +799,7 @@ static zend_bool can_ct_eval_func_call(zend_string *name, uint32_t num_args, zva || zend_string_equals_literal(name, "base64_encode") || zend_string_equals_literal(name, "imagetypes") || zend_string_equals_literal(name, "in_array") + || zend_string_equals_literal(name, "is_list") || zend_string_equals_literal(name, "ltrim") || zend_string_equals_literal(name, "php_sapi_name") || 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 return false; } } ZEND_HASH_FOREACH_END(); - return true; } return false; } diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 5d325515f7d1c..755c8b1e14f15 100755 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1408,6 +1408,8 @@ function is_string(mixed $value): bool {} function is_array(mixed $value): bool {} +function is_list(mixed $value): bool {} + function is_object(mixed $value): bool {} function is_scalar(mixed $value): bool {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index a5bf2cedad1cf..24a0b6a998ab7 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 4edb7cad23ccb051dbe267b3979e98892607c98f */ + * Stub hash: e7c27569f3028091cfa1faa7b262c9e98f5da8a9 */ 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) @@ -2074,6 +2074,8 @@ ZEND_END_ARG_INFO() #define arginfo_is_array arginfo_boolval +#define arginfo_is_list arginfo_boolval + #define arginfo_is_object arginfo_boolval #define arginfo_is_scalar arginfo_boolval @@ -2781,6 +2783,7 @@ ZEND_FUNCTION(is_float); ZEND_FUNCTION(is_numeric); ZEND_FUNCTION(is_string); ZEND_FUNCTION(is_array); +ZEND_FUNCTION(is_list); ZEND_FUNCTION(is_object); ZEND_FUNCTION(is_scalar); ZEND_FUNCTION(is_callable); @@ -3432,6 +3435,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(is_numeric, arginfo_is_numeric) ZEND_FE(is_string, arginfo_is_string) ZEND_FE(is_array, arginfo_is_array) + ZEND_FE(is_list, arginfo_is_list) ZEND_FE(is_object, arginfo_is_object) ZEND_FE(is_scalar, arginfo_is_scalar) ZEND_FE(is_callable, arginfo_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 5a8b2a0b9dc72..ce379d0d4b929 100644 --- a/ext/standard/type.c +++ b/ext/standard/type.c @@ -321,6 +321,25 @@ PHP_FUNCTION(is_array) } /* }}} */ +/* {{{ 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_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); + + RETURN_BOOL(zend_array_is_list(arrval)); +} +/* }}} */ + /* {{{ 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)