mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Fix #1742 - allow is_countable assertion to mean something
This commit is contained in:
parent
4660beb28e
commit
c81a9a8737
@ -1666,6 +1666,10 @@ class AssertionFinder
|
||||
if ($first_var_name) {
|
||||
$if_types[$first_var_name] = [[$prefix . 'iterable']];
|
||||
}
|
||||
} elseif (self::hasCountableCheck($expr)) {
|
||||
if ($first_var_name) {
|
||||
$if_types[$first_var_name] = [[$prefix . 'countable']];
|
||||
}
|
||||
} elseif ($class_exists_check_type = self::hasClassExistsCheck($expr)) {
|
||||
if ($first_var_name) {
|
||||
if ($class_exists_check_type === 2) {
|
||||
@ -2298,6 +2302,20 @@ class AssertionFinder
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Expr\FuncCall $stmt
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function hasCountableCheck(PhpParser\Node\Expr\FuncCall $stmt)
|
||||
{
|
||||
if ($stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->parts[0]) === 'is_countable') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Expr\FuncCall $stmt
|
||||
*
|
||||
|
@ -990,7 +990,7 @@ class TypeAnalyzer
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($input_type_part->isTraversable($codebase)) {
|
||||
if ($input_type_part->hasTraversableInterface($codebase)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -249,7 +249,7 @@ abstract class Atomic
|
||||
public function isIterable(Codebase $codebase)
|
||||
{
|
||||
return $this instanceof TIterable
|
||||
|| $this->isTraversable($codebase)
|
||||
|| $this->hasTraversableInterface($codebase)
|
||||
|| $this instanceof TArray
|
||||
|| $this instanceof ObjectLike;
|
||||
}
|
||||
@ -257,7 +257,17 @@ abstract class Atomic
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isTraversable(Codebase $codebase)
|
||||
public function isCountable(Codebase $codebase)
|
||||
{
|
||||
return $this->hasCountableInterface($codebase)
|
||||
|| $this instanceof TArray
|
||||
|| $this instanceof ObjectLike;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTraversableInterface(Codebase $codebase)
|
||||
{
|
||||
return $this instanceof TNamedObject
|
||||
&& (strtolower($this->value) === 'traversable'
|
||||
@ -269,6 +279,40 @@ abstract class Atomic
|
||||
$this->value,
|
||||
'Traversable'
|
||||
)))
|
||||
|| ($this->extra_types
|
||||
&& array_filter(
|
||||
$this->extra_types,
|
||||
function (Atomic $a) use ($codebase) : bool {
|
||||
return $a->hasTraversableInterface($codebase);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCountableInterface(Codebase $codebase)
|
||||
{
|
||||
return $this instanceof TNamedObject
|
||||
&& (strtolower($this->value) === 'countable'
|
||||
|| ($codebase->classOrInterfaceExists($this->value)
|
||||
&& ($codebase->classExtendsOrImplements(
|
||||
$this->value,
|
||||
'Countable'
|
||||
) || $codebase->interfaceExtends(
|
||||
$this->value,
|
||||
'Countable'
|
||||
)))
|
||||
|| ($this->extra_types
|
||||
&& array_filter(
|
||||
$this->extra_types,
|
||||
function (Atomic $a) use ($codebase) : bool {
|
||||
return $a->hasCountableInterface($codebase);
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -728,7 +728,11 @@ class Reconciler
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
if ($new_var_type === 'iterable' && !$existing_var_type->hasMixed()) {
|
||||
if ($new_var_type === 'iterable') {
|
||||
if ($existing_var_type->hasMixed()) {
|
||||
return new Type\Union([new Type\Atomic\TIterable]);
|
||||
}
|
||||
|
||||
$iterable_types = [];
|
||||
$did_remove_type = false;
|
||||
|
||||
@ -736,7 +740,57 @@ class Reconciler
|
||||
if ($type->isIterable($codebase)) {
|
||||
$iterable_types[] = $type;
|
||||
} elseif ($type instanceof TObject) {
|
||||
$iterable_types[] = new Type\Atomic\TCallableObject();
|
||||
$iterable_types[] = new Type\Atomic\TNamedObject('Traversable');
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$did_remove_type = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((!$iterable_types || !$did_remove_type) && !$is_equality) {
|
||||
if ($key && $code_location) {
|
||||
self::triggerIssueForImpossible(
|
||||
$existing_var_type,
|
||||
$old_var_type_string,
|
||||
$key,
|
||||
$new_var_type,
|
||||
!$did_remove_type,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($iterable_types) {
|
||||
return new Type\Union($iterable_types);
|
||||
}
|
||||
|
||||
$failed_reconciliation = 2;
|
||||
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
if ($new_var_type === 'countable') {
|
||||
if ($existing_var_type->hasMixed()) {
|
||||
return new Type\Union([
|
||||
new Type\Atomic\TArray([Type::getArrayKey(), Type::getMixed()]),
|
||||
new Type\Atomic\TNamedObject('Countable')
|
||||
]);
|
||||
}
|
||||
|
||||
$iterable_types = [];
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if ($type->isCountable($codebase)) {
|
||||
$iterable_types[] = $type;
|
||||
} elseif ($type instanceof TObject) {
|
||||
$iterable_types[] = new TNamedObject('Countable');
|
||||
$did_remove_type = true;
|
||||
} elseif ($type instanceof TNamedObject || $type instanceof Type\Atomic\TIterable) {
|
||||
$countable = new TNamedObject('Countable');
|
||||
$type->extra_types[$countable->getKey()] = $countable;
|
||||
$iterable_types[] = $type;
|
||||
$did_remove_type = true;
|
||||
} else {
|
||||
$did_remove_type = true;
|
||||
@ -1850,7 +1904,7 @@ class Reconciler
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
if ($new_var_type === 'iterable') {
|
||||
if ($new_var_type === 'iterable' || $new_var_type === 'countable') {
|
||||
$existing_var_type->removeType('array');
|
||||
}
|
||||
|
||||
|
@ -1713,6 +1713,18 @@ class FunctionCallTest extends TestCase
|
||||
/** @psalm-suppress TooFewArguments */
|
||||
min(0);',
|
||||
],
|
||||
'PHP73-allowIsCountableToInformType' => [
|
||||
'<?php
|
||||
function getObject() : iterable{
|
||||
return [];
|
||||
}
|
||||
|
||||
$iterableObject = getObject();
|
||||
|
||||
if (is_countable($iterableObject)) {
|
||||
if (count($iterableObject) === 0) {}
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user