diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index f89a9b7ed..64b1736f6 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -215,6 +215,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler if ($assertion === 'falsy' || $assertion === 'empty') { return self::reconcileFalsyOrEmpty( + $codebase, $assertion, $existing_var_type, $key, @@ -981,7 +982,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler } } - if (!$object_types) { + if (!$object_types || !$did_remove_type) { if ($key && $code_location) { self::triggerIssueForImpossible( $existing_var_type, @@ -2053,6 +2054,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler * @param 0|1|2 $failed_reconciliation */ private static function reconcileFalsyOrEmpty( + Codebase $codebase, string $assertion, Union $existing_var_type, ?string $key, @@ -2325,7 +2327,14 @@ class AssertionReconciler extends \Psalm\Type\Reconciler ) { $did_remove_type = true; - $existing_var_type->removeType($type_key); + if ($type instanceof TNamedObject + && $codebase->classlikes->classExists($type->value) + && $codebase->classlikes->classImplements($type->value, 'Countable') + ) { + // do nothing + } else { + $existing_var_type->removeType($type_key); + } } if ($type instanceof TTemplateParam) { diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 6a61687d3..6366927e4 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -185,7 +185,6 @@ class Reconciler foreach ($new_types as $key => $new_type_parts) { $has_negation = false; - $has_equality = false; $has_isset = false; $has_inverted_isset = false; $has_falsyish = false; @@ -198,9 +197,6 @@ class Reconciler case '!': $has_negation = true; break; - case '=': - case '~': - $has_equality = true; } $has_isset = $has_isset @@ -300,37 +296,6 @@ class Reconciler $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) { $changed_var_ids[$key] = true; } diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 8f9d8a7fd..df0baec8e 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -890,6 +890,41 @@ class AssertAnnotationTest extends TestCase return $value; }' ], + 'allowEmptyAssertionOnCountableObject' => [ + '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); + } + }', + ] ]; } diff --git a/tests/TypeReconciliation/RedundantConditionTest.php b/tests/TypeReconciliation/RedundantConditionTest.php index bc133f9ec..32c42d7ca 100644 --- a/tests/TypeReconciliation/RedundantConditionTest.php +++ b/tests/TypeReconciliation/RedundantConditionTest.php @@ -1167,7 +1167,7 @@ class RedundantConditionTest extends \Psalm\Tests\TestCase }', 'error_message' => 'TypeDoesNotContainType', ], - 'secondInterfaceAssertionIsRedundant' => [ + 'SKIPPED-secondInterfaceAssertionIsRedundant' => [ '