diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index 5f9980a8c06..f8af3275942 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -93,30 +93,21 @@ public static function reconcile( $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); $existing_var_type = $existing_var_type->getBuilder(); - if ($assertion_type instanceof TFalse && isset($existing_var_atomic_types['bool'])) { - $existing_var_type->removeType('bool'); - $existing_var_type->addType(new TTrue); - } elseif ($assertion_type instanceof TTrue && isset($existing_var_atomic_types['bool'])) { - $existing_var_type = $existing_var_type->getBuilder(); - $existing_var_type->removeType('bool'); - $existing_var_type->addType(new TFalse); - } else { - $simple_negated_type = SimpleNegatedAssertionReconciler::reconcile( - $statements_analyzer->getCodebase(), - $assertion, - $existing_var_type->freeze(), - $key, - $negated, - $code_location, - $suppressed_issues, - $failed_reconciliation, - $is_equality, - $inside_loop - ); - - if ($simple_negated_type) { - return $simple_negated_type; - } + $simple_negated_type = SimpleNegatedAssertionReconciler::reconcile( + $statements_analyzer->getCodebase(), + $assertion, + $existing_var_type->freeze(), + $key, + $negated, + $code_location, + $suppressed_issues, + $failed_reconciliation, + $is_equality, + $inside_loop + ); + + if ($simple_negated_type) { + return $simple_negated_type; } $assertion_type = $assertion->getAtomicType(); diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 50bab6e6cc7..e4a06a63c20 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -41,6 +41,7 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TEmptyMixed; +use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TInt; @@ -425,6 +426,32 @@ public static function reconcile( ); } + if ($assertion_type && $assertion_type instanceof TTrue) { + return self::reconcileTrue( + $assertion, + $existing_var_type, + $key, + $negated, + $code_location, + $suppressed_issues, + $failed_reconciliation, + $is_equality + ); + } + + if ($assertion_type && $assertion_type instanceof TFalse) { + return self::reconcileFalse( + $assertion, + $existing_var_type, + $key, + $negated, + $code_location, + $suppressed_issues, + $failed_reconciliation, + $is_equality + ); + } + if ($assertion_type && get_class($assertion_type) === TString::class) { return self::reconcileString( $assertion, @@ -1142,6 +1169,176 @@ private static function reconcileBool( : Type::getNever(); } + /** + * @param string[] $suppressed_issues + * @param Reconciler::RECONCILIATION_* $failed_reconciliation + */ + private static function reconcileFalse( + Assertion $assertion, + Union $existing_var_type, + ?string $key, + bool $negated, + ?CodeLocation $code_location, + array $suppressed_issues, + int &$failed_reconciliation, + bool $is_equality + ): Union { + if ($existing_var_type->hasMixed()) { + return Type::getFalse(); + } + if ($existing_var_type->hasScalar()) { + return Type::getFalse(); + } + + $old_var_type_string = $existing_var_type->getId(); + $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); + + $false_types = []; + $did_remove_type = false; + + foreach ($existing_var_atomic_types as $type) { + if ($type instanceof TFalse) { + $false_types[] = $type; + } elseif ($type instanceof TBool) { + $false_types[] = new TFalse(); + $did_remove_type = true; + } elseif ($type instanceof TTemplateParam && $type->as->isMixed()) { + $type = $type->replaceAs(Type::getFalse()); + $false_types[] = $type; + $did_remove_type = true; + } elseif ($type instanceof TTemplateParam) { + if ($type->as->hasScalar() || $type->as->hasMixed() || $type->as->hasBool()) { + $type = $type->replaceAs(self::reconcileFalse( + $assertion, + $type->as, + null, + false, + null, + $suppressed_issues, + $failed_reconciliation, + $is_equality + )); + + $false_types[] = $type; + } + + $did_remove_type = true; + } else { + $did_remove_type = true; + } + } + + if ((!$false_types || !$did_remove_type) && !$is_equality) { + if ($key && $code_location) { + self::triggerIssueForImpossible( + $existing_var_type, + $old_var_type_string, + $key, + $assertion, + !$did_remove_type, + $negated, + $code_location, + $suppressed_issues + ); + } + } + + if ($false_types) { + return new Union($false_types); + } + + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; + + return $existing_var_type->from_docblock + ? Type::getMixed() + : Type::getNever(); + } + + /** + * @param string[] $suppressed_issues + * @param Reconciler::RECONCILIATION_* $failed_reconciliation + */ + private static function reconcileTrue( + Assertion $assertion, + Union $existing_var_type, + ?string $key, + bool $negated, + ?CodeLocation $code_location, + array $suppressed_issues, + int &$failed_reconciliation, + bool $is_equality + ): Union { + if ($existing_var_type->hasMixed()) { + return Type::getTrue(); + } + if ($existing_var_type->hasScalar()) { + return Type::getTrue(); + } + + $old_var_type_string = $existing_var_type->getId(); + $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); + + $true_types = []; + $did_remove_type = false; + + foreach ($existing_var_atomic_types as $type) { + if ($type instanceof TTrue) { + $true_types[] = $type; + } elseif ($type instanceof TBool) { + $true_types[] = new TTrue(); + $did_remove_type = true; + } elseif ($type instanceof TTemplateParam && $type->as->isMixed()) { + $type = $type->replaceAs(Type::getTrue()); + $true_types[] = $type; + $did_remove_type = true; + } elseif ($type instanceof TTemplateParam) { + if ($type->as->hasScalar() || $type->as->hasMixed() || $type->as->hasBool()) { + $type = $type->replaceAs(self::reconcileTrue( + $assertion, + $type->as, + null, + false, + null, + $suppressed_issues, + $failed_reconciliation, + $is_equality + )); + + $true_types[] = $type; + } + + $did_remove_type = true; + } else { + $did_remove_type = true; + } + } + + if ((!$true_types || !$did_remove_type) && !$is_equality) { + if ($key && $code_location) { + self::triggerIssueForImpossible( + $existing_var_type, + $old_var_type_string, + $key, + $assertion, + !$did_remove_type, + $negated, + $code_location, + $suppressed_issues + ); + } + } + + if ($true_types) { + return new Union($true_types); + } + + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; + + return $existing_var_type->from_docblock + ? Type::getMixed() + : Type::getNever(); + } + /** * @param string[] $suppressed_issues * @param Reconciler::RECONCILIATION_* $failed_reconciliation diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 05c787eb2e5..48bbabdbf0a 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -58,6 +58,7 @@ use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Atomic\TTrue; use Psalm\Type\Reconciler; use Psalm\Type\Union; @@ -402,6 +403,19 @@ public static function reconcile( ); } + if ($assertion_type instanceof TTrue && !$existing_var_type->hasMixed()) { + return self::reconcileTrue( + $assertion, + $existing_var_type, + $key, + $negated, + $code_location, + $suppressed_issues, + $failed_reconciliation, + $is_equality + ); + } + if ($assertion_type instanceof TCallable) { return self::reconcileCallable( $existing_var_type @@ -702,6 +716,14 @@ private static function reconcileFalse( $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; + if (isset($types['scalar'])) { + $did_remove_type = true; + } + if (isset($types['bool'])) { + $did_remove_type = true; + $types[] = new TTrue(); + unset($types['bool']); + } if (isset($types['false'])) { $did_remove_type = true; unset($types['false']); @@ -756,6 +778,86 @@ private static function reconcileFalse( : Type::getNever(); } + /** + * @param string[] $suppressed_issues + * @param Reconciler::RECONCILIATION_* $failed_reconciliation + */ + private static function reconcileTrue( + Assertion $assertion, + Union $existing_var_type, + ?string $key, + bool $negated, + ?CodeLocation $code_location, + array $suppressed_issues, + int &$failed_reconciliation, + bool $is_equality + ): Union { + $types = $existing_var_type->getAtomicTypes(); + $old_var_type_string = $existing_var_type->getId(); + $did_remove_type = false; + + if (isset($types['scalar'])) { + $did_remove_type = true; + } + if (isset($types['bool'])) { + $did_remove_type = true; + $types[] = new TFalse(); + unset($types['bool']); + } + if (isset($types['true'])) { + $did_remove_type = true; + unset($types['true']); + } + + foreach ($types as &$type) { + if ($type instanceof TTemplateParam) { + $new = $type->replaceAs(self::reconcileTrue( + $assertion, + $type->as, + null, + false, + null, + $suppressed_issues, + $failed_reconciliation, + $is_equality + )); + + $did_remove_type = $did_remove_type || $new !== $type; + $type = $new; + } + } + unset($type); + + if (!$did_remove_type || !$types) { + if ($key && $code_location && !$is_equality) { + self::triggerIssueForImpossible( + $existing_var_type, + $old_var_type_string, + $key, + $assertion, + !$did_remove_type, + $negated, + $code_location, + $suppressed_issues + ); + } + + if (!$did_remove_type) { + $failed_reconciliation = Reconciler::RECONCILIATION_REDUNDANT; + } + } + + if ($types) { + return $existing_var_type->setTypes($types); + } + + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; + + return $existing_var_type->from_docblock + ? Type::getMixed() + : Type::getNever(); + } + /** * @param Falsy|Empty_ $assertion * @param string[] $suppressed_issues