diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 2c801dec9..4cd4dfc3d 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -11334,7 +11334,7 @@ return [ 'SimpleXMLElement::xpath' => ['SimpleXMLElement[]|false', 'path'=>'string'], 'sin' => ['float', 'num'=>'float'], 'sinh' => ['float', 'num'=>'float'], -'sizeof' => ['int', 'value'=>'Countable|array', 'mode='=>'int'], +'sizeof' => ['int<0, max>', 'value'=>'Countable|array', 'mode='=>'int'], 'sleep' => ['int', 'seconds'=>'0|positive-int'], 'snmp2_get' => ['string|false', 'hostname'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], 'snmp2_getnext' => ['string|false', 'hostname'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index b2cc5c427..9f3727636 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -497,6 +497,10 @@ return [ 'old' => ['int<0, max>', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'], 'new' => ['int<0, max>', 'value'=>'Countable|array', 'mode='=>'int'], ], + 'sizeof' => [ + 'old' => ['int<0, max>', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'], + 'new' => ['int<0, max>', 'value'=>'Countable|array', 'mode='=>'int'], + ], 'count_chars' => [ 'old' => ['array|false', 'input'=>'string', 'mode='=>'0|1|2'], 'new' => ['array', 'input'=>'string', 'mode='=>'0|1|2'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index ca5028008..c58cb3d06 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14030,7 +14030,7 @@ return [ 'simplexml_load_string' => ['SimpleXMLElement|false', 'data'=>'string', 'class_name='=>'?string', 'options='=>'int', 'namespace_or_prefix='=>'string', 'is_prefix='=>'bool'], 'sin' => ['float', 'num'=>'float'], 'sinh' => ['float', 'num'=>'float'], - 'sizeof' => ['int', 'value'=>'Countable|array', 'mode='=>'int'], + 'sizeof' => ['int<0, max>', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'], 'sleep' => ['int|false', 'seconds'=>'0|positive-int'], 'snmp2_get' => ['string|false', 'hostname'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], 'snmp2_getnext' => ['string|false', 'hostname'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index cd3d910a9..5f4bcdf0f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -1542,7 +1542,7 @@ class AssertionFinder ) { if ($conditional->left instanceof PhpParser\Node\Expr\FuncCall && $conditional->left->name instanceof PhpParser\Node\Name - && strtolower($conditional->left->name->parts[0]) === 'count' + && in_array(strtolower($conditional->left->name->parts[0]), ['count', 'sizeof']) && $conditional->left->getArgs() && ($conditional instanceof BinaryOp\Greater || $conditional instanceof BinaryOp\GreaterOrEqual) ) { @@ -1551,7 +1551,7 @@ class AssertionFinder $comparison_adjustment = $conditional instanceof BinaryOp\Greater ? 1 : 0; } elseif ($conditional->right instanceof PhpParser\Node\Expr\FuncCall && $conditional->right->name instanceof PhpParser\Node\Name - && strtolower($conditional->right->name->parts[0]) === 'count' + && in_array(strtolower($conditional->right->name->parts[0]), ['count', 'sizeof']) && $conditional->right->getArgs() && ($conditional instanceof BinaryOp\Smaller || $conditional instanceof BinaryOp\SmallerOrEqual) ) { @@ -1584,7 +1584,7 @@ class AssertionFinder ) { $left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall && $conditional->left->name instanceof PhpParser\Node\Name - && strtolower($conditional->left->name->parts[0]) === 'count' + && in_array(strtolower($conditional->left->name->parts[0]), ['count', 'sizeof']) && $conditional->left->getArgs(); $operator_less_than_or_equal = @@ -1603,7 +1603,7 @@ class AssertionFinder $right_count = $conditional->right instanceof PhpParser\Node\Expr\FuncCall && $conditional->right->name instanceof PhpParser\Node\Name - && strtolower($conditional->right->name->parts[0]) === 'count' + && in_array(strtolower($conditional->right->name->parts[0]), ['count', 'sizeof']) && $conditional->right->getArgs(); $operator_greater_than_or_equal = @@ -1633,7 +1633,7 @@ class AssertionFinder ) { $left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall && $conditional->left->name instanceof PhpParser\Node\Name - && strtolower($conditional->left->name->parts[0]) === 'count' + && in_array(strtolower($conditional->left->name->parts[0]), ['count', 'sizeof']) && $conditional->left->getArgs(); if ($left_count && $conditional->right instanceof PhpParser\Node\Scalar\LNumber) { @@ -1644,7 +1644,7 @@ class AssertionFinder $right_count = $conditional->right instanceof PhpParser\Node\Expr\FuncCall && $conditional->right->name instanceof PhpParser\Node\Name - && strtolower($conditional->right->name->parts[0]) === 'count' + && in_array(strtolower($conditional->right->name->parts[0]), ['count', 'sizeof']) && $conditional->right->getArgs(); if ($right_count && $conditional->left instanceof PhpParser\Node\Scalar\LNumber) { @@ -1785,7 +1785,7 @@ class AssertionFinder ) { $left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall && $conditional->left->name instanceof PhpParser\Node\Name - && strtolower($conditional->left->name->parts[0]) === 'count'; + && in_array(strtolower($conditional->left->name->parts[0]), ['count', 'sizeof']); $right_number = $conditional->right instanceof PhpParser\Node\Scalar\LNumber && $conditional->right->value === ( @@ -2044,7 +2044,8 @@ class AssertionFinder protected static function hasNonEmptyCountCheck(PhpParser\Node\Expr\FuncCall $stmt): bool { - return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->parts[0]) === 'count'; + return $stmt->name instanceof PhpParser\Node\Name && + in_array(strtolower($stmt->name->parts[0]), ['count', 'sizeof']); } protected static function hasArrayKeyExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 6c40bf783..76ba4f08c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -354,6 +354,7 @@ class FunctionCallReturnTypeFetcher } else { switch ($call_map_key) { case 'count': + case 'sizeof': if (($first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value))) { $atomic_types = $first_arg_type->getAtomicTypes(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php index 677d36a57..c16948048 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php @@ -81,7 +81,8 @@ class MatchAnalyzer && ($stmt->cond->name->parts === ['get_class'] || $stmt->cond->name->parts === ['gettype'] || $stmt->cond->name->parts === ['get_debug_type'] - || $stmt->cond->name->parts === ['count']) + || $stmt->cond->name->parts === ['count'] + || $stmt->cond->name->parts === ['sizeof']) && $stmt->cond->getArgs() ) { $first_arg = $stmt->cond->getArgs()[0]; diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index 9ba277fec..50e3f60fb 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -555,7 +555,7 @@ class Functions return true; } - if ($function_id === 'count' && isset($args[0]) && $type_provider) { + if (in_array($function_id, ['count', 'sizeof']) && isset($args[0]) && $type_provider) { $count_type = $type_provider->getType($args[0]->value); if ($count_type) { diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 113458d2d..1a9a63f1b 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -145,6 +145,22 @@ class ArrayAccessTest extends TestCase $this->analyzeFile('somefile.php', new Context()); } + public function testNoIssueWhenUsingArrayValuesOnNonEmptyArrayCheckedWithSizeof(): void + { + Config::getInstance()->ensure_array_int_offsets_exist = true; + + $this->addFile( + 'somefile.php', + 'analyzeFile('somefile.php', new Context()); + } + public function testNoIssueAfterManyIssets(): void { Config::getInstance()->ensure_array_int_offsets_exist = true;