Skip to content
Permalink
Browse files

Use more accurate way to determine list size

  • Loading branch information
muglug committed Nov 26, 2019
1 parent 4e594e0 commit f97a8f0d5bb99e0aa1c959e62a7998d791eeb57a
@@ -210,7 +210,8 @@ public static function scrapeAssertions(
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual
) {
$count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional);
$min_count = null;
$count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional, $min_count);
$typed_value_position = self::hasTypedValueComparison($conditional, $source);
if ($count_equality_position) {
@@ -231,7 +232,11 @@ public static function scrapeAssertions(
if (self::hasReconcilableNonEmptyCountEqualityCheck($conditional)) {
$if_types[$var_name] = [['non-empty-countable']];
} else {
$if_types[$var_name] = [['=non-empty-countable']];
if ($min_count) {
$if_types[$var_name] = [['=has-at-least-' . $min_count]];
} else {
$if_types[$var_name] = [['=non-empty-countable']];
}
}
}
@@ -265,7 +270,8 @@ public static function scrapeAssertions(
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual
) {
$count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional);
$min_count = null;
$count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional, $min_count);
$typed_value_position = self::hasTypedValueComparison($conditional, $source);
if ($count_equality_position) {
@@ -283,7 +289,11 @@ public static function scrapeAssertions(
);
if ($var_name) {
$if_types[$var_name] = [['=non-empty-countable']];
if ($min_count) {
$if_types[$var_name] = [['=has-at-least-' . $min_count]];
} else {
$if_types[$var_name] = [['=non-empty-countable']];
}
}
return $if_types;
@@ -434,7 +444,8 @@ private static function scrapeEqualityAssertions(
$empty_array_position = self::hasEmptyArrayVariable($conditional);
$gettype_position = self::hasGetTypeCheck($conditional);
$getclass_position = self::hasGetClassCheck($conditional, $source);
$count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional);
$min_count = null;
$count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional, $min_count);
$typed_value_position = self::hasTypedValueComparison($conditional, $source);
if ($null_position !== null) {
@@ -835,7 +846,11 @@ private static function scrapeEqualityAssertions(
);
if ($var_name) {
$if_types[$var_name] = [['=non-empty-countable']];
if ($min_count) {
$if_types[$var_name] = [['=has-at-least-' . $min_count]];
} else {
$if_types[$var_name] = [['=non-empty-countable']];
}
}
return $if_types;
@@ -2244,24 +2259,33 @@ protected static function hasGetClassCheck(
*
* @return false|int
*/
protected static function hasNonEmptyCountEqualityCheck(PhpParser\Node\Expr\BinaryOp $conditional)
{
protected static function hasNonEmptyCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$min_count
) {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& strtolower($conditional->left->name->parts[0]) === 'count'
&& $conditional->left->args;
$right_number = $conditional->right instanceof PhpParser\Node\Scalar\LNumber
&& $conditional->right->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 0 : 1);
$operator_greater_than_or_equal =
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual;
if ($left_count && $right_number && $operator_greater_than_or_equal) {
if ($left_count
&& $conditional->right instanceof PhpParser\Node\Scalar\LNumber
&& $operator_greater_than_or_equal
&& $conditional->right->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
? 0
: 1
)
) {
$min_count = $conditional->right->value +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 1 : 0);
return self::ASSIGNMENT_TO_RIGHT;
}
@@ -2270,17 +2294,22 @@ protected static function hasNonEmptyCountEqualityCheck(PhpParser\Node\Expr\Bina
&& strtolower($conditional->right->name->parts[0]) === 'count'
&& $conditional->right->args;
$left_number = $conditional->left instanceof PhpParser\Node\Scalar\LNumber
&& $conditional->left->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 0 : 1);
$operator_less_than_or_equal =
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual;
if ($right_count && $left_number && $operator_less_than_or_equal) {
if ($right_count
&& $conditional->left instanceof PhpParser\Node\Scalar\LNumber
&& $operator_less_than_or_equal
&& $conditional->left->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 0 : 1
)
) {
$min_count = $conditional->left->value +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 1 : 0);
return self::ASSIGNMENT_TO_LEFT;
}
@@ -679,7 +679,9 @@ public static function getArrayAccessTypeGivenOffset(
} elseif ($type instanceof TList) {
// if we're assigning to an empty array with a key offset, refashion that array
if (!$in_assignment) {
if ($key_value !== 0 || !$type instanceof TNonEmptyList) {
if (!$type instanceof TNonEmptyList
|| ($key_value > 0 && $key_value > ($type->count - 1))
) {
$expected_offset_type = Type::getInt();
if ($codebase->config->ensure_array_int_offsets_exist) {
@@ -120,7 +120,10 @@ public static function reconcile(
return Type::getMixed($inside_loop);
}
if ($assertion === 'array-key-exists' || $assertion === 'non-empty-countable') {
if ($assertion === 'array-key-exists'
|| $assertion === 'non-empty-countable'
|| strpos($assertion, 'has-at-least-') === 0
) {
return Type::getMixed();
}
@@ -365,7 +368,20 @@ public static function reconcile(
$code_location,
$suppressed_issues,
$failed_reconciliation,
$is_equality
$is_equality,
null
);
}
if (substr($assertion, 0, 13) === 'has-at-least-') {
return self::reconcileNonEmptyCountable(
$existing_var_type,
$key,
$code_location,
$suppressed_issues,
$failed_reconciliation,
$is_equality,
(int) substr($assertion, 13)
);
}
@@ -790,7 +806,8 @@ private static function reconcileNonEmptyCountable(
?CodeLocation $code_location,
array $suppressed_issues,
int &$failed_reconciliation,
bool $is_equality
bool $is_equality,
?int $min_count
) : Union {
$old_var_type_string = $existing_var_type->getId();
@@ -812,15 +829,21 @@ private static function reconcileNonEmptyCountable(
)
);
}
} elseif ($array_atomic_type instanceof TList
&& !$array_atomic_type instanceof Type\Atomic\TNonEmptyList
) {
$did_remove_type = true;
$existing_var_type->addType(
new Type\Atomic\TNonEmptyList(
} elseif ($array_atomic_type instanceof TList) {
if (!$array_atomic_type instanceof Type\Atomic\TNonEmptyList
|| ($array_atomic_type->count < $min_count)
) {
$non_empty_list = new Type\Atomic\TNonEmptyList(
$array_atomic_type->type_param
)
);
);
if ($min_count) {
$non_empty_list->count = $min_count;
}
$did_remove_type = true;
$existing_var_type->addType($non_empty_list);
}
} elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
foreach ($array_atomic_type->properties as $property_type) {
if ($property_type->possibly_undefined) {
@@ -229,6 +229,111 @@ function takesList(array $arr) : void {
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
/**
* @return void
*/
public function testEnsureListOffsetExistsAfterCountValueInRange()
{
\Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true;
$this->addFile(
'somefile.php',
'<?php
/** @param list<string> $arr */
function takesList(array $arr) : void {
if (count($arr) >= 3) {
echo $arr[0];
echo $arr[1];
echo $arr[2];
}
if (count($arr) > 2) {
echo $arr[0];
echo $arr[1];
echo $arr[2];
}
if (count($arr) === 3) {
echo $arr[0];
echo $arr[1];
echo $arr[2];
}
if (3 === count($arr)) {
echo $arr[0];
echo $arr[1];
echo $arr[2];
}
if (3 <= count($arr)) {
echo $arr[0];
echo $arr[1];
echo $arr[2];
}
if (2 < count($arr)) {
echo $arr[0];
echo $arr[1];
echo $arr[2];
}
}'
);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
/**
* @return void
*/
public function testEnsureListOffsetExistsAfterCountValueOutOfRange()
{
\Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true;
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessage('PossiblyUndefinedIntArrayOffset');
$this->addFile(
'somefile.php',
'<?php
/** @param list<string> $arr */
function takesList(array $arr) : void {
if (count($arr) >= 2) {
echo $arr[0];
echo $arr[1];
echo $arr[2];
}
}'
);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
/**
* @return void
*/
public function testEnsureListOffsetExistsAfterCountValueOutOfRangeSmallerThan()
{
\Psalm\Config::getInstance()->ensure_array_int_offsets_exist = true;
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessage('PossiblyUndefinedIntArrayOffset');
$this->addFile(
'somefile.php',
'<?php
/** @param list<string> $arr */
function takesList(array $arr) : void {
if (2 <= count($arr)) {
echo $arr[0];
echo $arr[1];
echo $arr[2];
}
}'
);
$this->analyzeFile('somefile.php', new \Psalm\Context());
}
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/

0 comments on commit f97a8f0

Please sign in to comment.
You can’t perform that action at this time.