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 ($first_var_name) {
|
||||||
$if_types[$first_var_name] = [[$prefix . 'iterable']];
|
$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)) {
|
} elseif ($class_exists_check_type = self::hasClassExistsCheck($expr)) {
|
||||||
if ($first_var_name) {
|
if ($first_var_name) {
|
||||||
if ($class_exists_check_type === 2) {
|
if ($class_exists_check_type === 2) {
|
||||||
@ -2298,6 +2302,20 @@ class AssertionFinder
|
|||||||
return false;
|
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
|
* @param PhpParser\Node\Expr\FuncCall $stmt
|
||||||
*
|
*
|
||||||
|
@ -990,7 +990,7 @@ class TypeAnalyzer
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($input_type_part->isTraversable($codebase)) {
|
if ($input_type_part->hasTraversableInterface($codebase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,7 +249,7 @@ abstract class Atomic
|
|||||||
public function isIterable(Codebase $codebase)
|
public function isIterable(Codebase $codebase)
|
||||||
{
|
{
|
||||||
return $this instanceof TIterable
|
return $this instanceof TIterable
|
||||||
|| $this->isTraversable($codebase)
|
|| $this->hasTraversableInterface($codebase)
|
||||||
|| $this instanceof TArray
|
|| $this instanceof TArray
|
||||||
|| $this instanceof ObjectLike;
|
|| $this instanceof ObjectLike;
|
||||||
}
|
}
|
||||||
@ -257,7 +257,17 @@ abstract class Atomic
|
|||||||
/**
|
/**
|
||||||
* @return bool
|
* @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
|
return $this instanceof TNamedObject
|
||||||
&& (strtolower($this->value) === 'traversable'
|
&& (strtolower($this->value) === 'traversable'
|
||||||
@ -269,6 +279,40 @@ abstract class Atomic
|
|||||||
$this->value,
|
$this->value,
|
||||||
'Traversable'
|
'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();
|
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 = [];
|
$iterable_types = [];
|
||||||
$did_remove_type = false;
|
$did_remove_type = false;
|
||||||
|
|
||||||
@ -736,7 +740,57 @@ class Reconciler
|
|||||||
if ($type->isIterable($codebase)) {
|
if ($type->isIterable($codebase)) {
|
||||||
$iterable_types[] = $type;
|
$iterable_types[] = $type;
|
||||||
} elseif ($type instanceof TObject) {
|
} 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;
|
$did_remove_type = true;
|
||||||
} else {
|
} else {
|
||||||
$did_remove_type = true;
|
$did_remove_type = true;
|
||||||
@ -1850,7 +1904,7 @@ class Reconciler
|
|||||||
return $existing_var_type;
|
return $existing_var_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($new_var_type === 'iterable') {
|
if ($new_var_type === 'iterable' || $new_var_type === 'countable') {
|
||||||
$existing_var_type->removeType('array');
|
$existing_var_type->removeType('array');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1713,6 +1713,18 @@ class FunctionCallTest extends TestCase
|
|||||||
/** @psalm-suppress TooFewArguments */
|
/** @psalm-suppress TooFewArguments */
|
||||||
min(0);',
|
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