Skip to content

Commit

Permalink
Add is_list function
Browse files Browse the repository at this point in the history
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 <tysonandre775@hotmail.com>
Co-Authored-By: Dusk <dusk@woofle.net>
  • Loading branch information
duskwuff and TysonAndre committed Dec 12, 2020
1 parent 70c22de commit 293dc5f
Show file tree
Hide file tree
Showing 7 changed files with 152 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
24 changes: 3 additions & 21 deletions ext/json/json_encoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,10 @@ static int php_json_escape_string(
static int php_json_determine_array_type(zval *val) /* {{{ */
{
int i;
HashTable *myht = Z_ARRVAL_P(val);
zend_array *myht = Z_ARRVAL_P(val);

i = myht ? zend_hash_num_elements(myht) : 0;
if (i > 0) {
zend_string *key;
zend_ulong index, idx;

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
2 changes: 1 addition & 1 deletion ext/opcache/Optimizer/sccp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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;
}
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 @@ -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 {}
Expand Down
6 changes: 5 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: 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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
94 changes: 94 additions & 0 deletions ext/standard/tests/general_functions/is_list.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
--TEST--
Test is_list() function
--FILE--
<?php

function test_is_list(string $desc, $val) : void {
printf("%s: %s\n", $desc, is_list($val) ? "true" : "false");
}

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: 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
20 changes: 20 additions & 0 deletions ext/standard/type.c
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,26 @@ 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);

/* Empty arrays are lists */
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)
Expand Down

0 comments on commit 293dc5f

Please sign in to comment.