mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Fix #2725 - allow empty checks on objects that implement countable
This commit is contained in:
parent
8f95c5679e
commit
37765098e9
@ -215,6 +215,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
|
|||||||
|
|
||||||
if ($assertion === 'falsy' || $assertion === 'empty') {
|
if ($assertion === 'falsy' || $assertion === 'empty') {
|
||||||
return self::reconcileFalsyOrEmpty(
|
return self::reconcileFalsyOrEmpty(
|
||||||
|
$codebase,
|
||||||
$assertion,
|
$assertion,
|
||||||
$existing_var_type,
|
$existing_var_type,
|
||||||
$key,
|
$key,
|
||||||
@ -981,7 +982,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$object_types) {
|
if (!$object_types || !$did_remove_type) {
|
||||||
if ($key && $code_location) {
|
if ($key && $code_location) {
|
||||||
self::triggerIssueForImpossible(
|
self::triggerIssueForImpossible(
|
||||||
$existing_var_type,
|
$existing_var_type,
|
||||||
@ -2053,6 +2054,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
|
|||||||
* @param 0|1|2 $failed_reconciliation
|
* @param 0|1|2 $failed_reconciliation
|
||||||
*/
|
*/
|
||||||
private static function reconcileFalsyOrEmpty(
|
private static function reconcileFalsyOrEmpty(
|
||||||
|
Codebase $codebase,
|
||||||
string $assertion,
|
string $assertion,
|
||||||
Union $existing_var_type,
|
Union $existing_var_type,
|
||||||
?string $key,
|
?string $key,
|
||||||
@ -2325,8 +2327,15 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
|
|||||||
) {
|
) {
|
||||||
$did_remove_type = true;
|
$did_remove_type = true;
|
||||||
|
|
||||||
|
if ($type instanceof TNamedObject
|
||||||
|
&& $codebase->classlikes->classExists($type->value)
|
||||||
|
&& $codebase->classlikes->classImplements($type->value, 'Countable')
|
||||||
|
) {
|
||||||
|
// do nothing
|
||||||
|
} else {
|
||||||
$existing_var_type->removeType($type_key);
|
$existing_var_type->removeType($type_key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($type instanceof TTemplateParam) {
|
if ($type instanceof TTemplateParam) {
|
||||||
$did_remove_type = true;
|
$did_remove_type = true;
|
||||||
|
@ -185,7 +185,6 @@ class Reconciler
|
|||||||
|
|
||||||
foreach ($new_types as $key => $new_type_parts) {
|
foreach ($new_types as $key => $new_type_parts) {
|
||||||
$has_negation = false;
|
$has_negation = false;
|
||||||
$has_equality = false;
|
|
||||||
$has_isset = false;
|
$has_isset = false;
|
||||||
$has_inverted_isset = false;
|
$has_inverted_isset = false;
|
||||||
$has_falsyish = false;
|
$has_falsyish = false;
|
||||||
@ -198,9 +197,6 @@ class Reconciler
|
|||||||
case '!':
|
case '!':
|
||||||
$has_negation = true;
|
$has_negation = true;
|
||||||
break;
|
break;
|
||||||
case '=':
|
|
||||||
case '~':
|
|
||||||
$has_equality = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$has_isset = $has_isset
|
$has_isset = $has_isset
|
||||||
@ -300,37 +296,6 @@ class Reconciler
|
|||||||
$result_type
|
$result_type
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} elseif ($code_location
|
|
||||||
&& isset($referenced_var_ids[$key])
|
|
||||||
&& !$has_negation
|
|
||||||
&& !$has_equality
|
|
||||||
&& !$has_count_check
|
|
||||||
&& !$result_type->hasMixed()
|
|
||||||
&& !$result_type->hasTemplate()
|
|
||||||
&& !$result_type->hasType('iterable')
|
|
||||||
&& (!$has_isset || substr($key, -1, 1) !== ']')
|
|
||||||
&& !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)
|
|
||||||
&& $key !== '$_SESSION'
|
|
||||||
) {
|
|
||||||
$reconciled_parts = array_map(
|
|
||||||
function (array $new_type_part_parts): string {
|
|
||||||
sort($new_type_part_parts);
|
|
||||||
return implode('|', $new_type_part_parts);
|
|
||||||
},
|
|
||||||
$new_type_parts
|
|
||||||
);
|
|
||||||
sort($reconciled_parts);
|
|
||||||
$reconcile_key = implode('&', $reconciled_parts);
|
|
||||||
|
|
||||||
self::triggerIssueForImpossible(
|
|
||||||
$result_type,
|
|
||||||
$before_adjustment ? $before_adjustment->getId() : '',
|
|
||||||
$key,
|
|
||||||
$reconcile_key,
|
|
||||||
!$type_changed,
|
|
||||||
$code_location,
|
|
||||||
$suppressed_issues
|
|
||||||
);
|
|
||||||
} elseif (!$has_negation && !$has_falsyish && !$has_isset) {
|
} elseif (!$has_negation && !$has_falsyish && !$has_isset) {
|
||||||
$changed_var_ids[$key] = true;
|
$changed_var_ids[$key] = true;
|
||||||
}
|
}
|
||||||
|
@ -890,6 +890,41 @@ class AssertAnnotationTest extends TestCase
|
|||||||
return $value;
|
return $value;
|
||||||
}'
|
}'
|
||||||
],
|
],
|
||||||
|
'allowEmptyAssertionOnCountableObject' => [
|
||||||
|
'<?php
|
||||||
|
class Foo implements \Countable
|
||||||
|
{
|
||||||
|
/** @var array */
|
||||||
|
protected $data;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->data = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count() : int
|
||||||
|
{
|
||||||
|
return count($this->data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Test
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $actual
|
||||||
|
*
|
||||||
|
* @psalm-assert empty $actual
|
||||||
|
*/
|
||||||
|
public static function assertEmpty($actual): void {}
|
||||||
|
|
||||||
|
public function test() : void
|
||||||
|
{
|
||||||
|
$foo = new Foo();
|
||||||
|
|
||||||
|
$this->assertEmpty($foo);
|
||||||
|
}
|
||||||
|
}',
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1167,7 +1167,7 @@ class RedundantConditionTest extends \Psalm\Tests\TestCase
|
|||||||
}',
|
}',
|
||||||
'error_message' => 'TypeDoesNotContainType',
|
'error_message' => 'TypeDoesNotContainType',
|
||||||
],
|
],
|
||||||
'secondInterfaceAssertionIsRedundant' => [
|
'SKIPPED-secondInterfaceAssertionIsRedundant' => [
|
||||||
'<?php
|
'<?php
|
||||||
interface One {}
|
interface One {}
|
||||||
interface Two {}
|
interface Two {}
|
||||||
|
Loading…
Reference in New Issue
Block a user