diff --git a/psalm.xml.dist b/psalm.xml.dist
index b99369433..0ede744bf 100644
--- a/psalm.xml.dist
+++ b/psalm.xml.dist
@@ -138,6 +138,8 @@
+
+
diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php
index ca22ce045..ea6560554 100644
--- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php
+++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php
@@ -45,13 +45,7 @@ use function substr;
class NegatedAssertionReconciler extends Reconciler
{
/**
- * @param string $assertion
- * @param bool $is_strict_equality
- * @param bool $is_loose_equality
- * @param array> $template_type_map
- * @param string $old_var_type_string
- * @param string|null $key
- * @param CodeLocation|null $code_location
+ * @param array> $template_type_map
* @param string[] $suppressed_issues
* @param 0|1|2 $failed_reconciliation
*
@@ -59,16 +53,16 @@ class NegatedAssertionReconciler extends Reconciler
*/
public static function reconcile(
StatementsAnalyzer $statements_analyzer,
- $assertion,
- $is_strict_equality,
- $is_loose_equality,
+ string $assertion,
+ bool $is_strict_equality,
+ bool $is_loose_equality,
Type\Union $existing_var_type,
array $template_type_map,
- $old_var_type_string,
- $key,
- $code_location,
- $suppressed_issues,
- &$failed_reconciliation
+ string $old_var_type_string,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation
) {
$is_equality = $is_strict_equality || $is_loose_equality;
@@ -151,146 +145,19 @@ class NegatedAssertionReconciler extends Reconciler
$existing_var_atomic_types = $existing_var_type->getAtomicTypes();
- if ($assertion === 'object' && !$existing_var_type->hasMixed()) {
- return self::reconcileObject(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
+ $simple_negated_type = SimpleNegatedAssertionReconciler::reconcile(
+ $assertion,
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality,
+ $is_strict_equality
+ );
- if ($assertion === 'scalar' && !$existing_var_type->hasMixed()) {
- return self::reconcileScalar(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
-
- if ($assertion === 'bool' && !$existing_var_type->hasMixed()) {
- return self::reconcileBool(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
-
- if ($assertion === 'numeric' && !$existing_var_type->hasMixed()) {
- return self::reconcileNumeric(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
-
- if ($assertion === 'float' && !$existing_var_type->hasMixed()) {
- return self::reconcileFloat(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
-
- if ($assertion === 'int' && !$existing_var_type->hasMixed()) {
- return self::reconcileInt(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
-
- if ($assertion === 'string' && !$existing_var_type->hasMixed()) {
- return self::reconcileString(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
-
- if ($assertion === 'array' && !$existing_var_type->hasMixed()) {
- return self::reconcileArray(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
-
- if ($assertion === 'falsy' || $assertion === 'empty') {
- return self::reconcileFalsyOrEmpty(
- $assertion,
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality,
- $is_strict_equality
- );
- }
-
- if ($assertion === 'null' && !$existing_var_type->hasMixed()) {
- return self::reconcileNull(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality
- );
- }
-
- if ($assertion === 'non-empty-countable') {
- return self::reconcileNonEmptyCountable(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality,
- null
- );
- }
-
- if (substr($assertion, 0, 13) === 'has-at-least-') {
- return self::reconcileNonEmptyCountable(
- $existing_var_type,
- $key,
- $code_location,
- $suppressed_issues,
- $failed_reconciliation,
- $is_equality,
- (int) substr($assertion, 13)
- );
- }
-
- if ($assertion === 'callable') {
- return self::reconcileCallable(
- $existing_var_type
- );
+ if ($simple_negated_type) {
+ return $simple_negated_type;
}
if ($assertion === 'iterable' || $assertion === 'countable') {
@@ -431,1053 +298,6 @@ class NegatedAssertionReconciler extends Reconciler
return $existing_var_type;
}
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileCallable(
- Type\Union $existing_var_type
- ) : Type\Union {
- foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $type) {
- if ($type instanceof Type\Atomic\TLiteralString
- && \Psalm\Internal\Codebase\CallMap::inCallMap($type->value)
- ) {
- $existing_var_type->removeType($atomic_key);
- }
-
- if ($type->isCallableType()) {
- $existing_var_type->removeType($atomic_key);
- }
- }
-
- return $existing_var_type;
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileBool(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $non_bool_types = [];
- $did_remove_type = false;
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if (!$type->as->hasBool()) {
- $non_bool_types[] = $type;
- }
-
- $did_remove_type = true;
- } elseif (!$type instanceof TBool
- || ($is_equality && get_class($type) === TBool::class)
- ) {
- if ($type instanceof TScalar) {
- $did_remove_type = true;
- $non_bool_types[] = new TString();
- $non_bool_types[] = new TInt();
- $non_bool_types[] = new TFloat();
- } else {
- $non_bool_types[] = $type;
- }
- } else {
- $did_remove_type = true;
- }
- }
-
- if (!$did_remove_type || !$non_bool_types) {
- if ($key && $code_location && !$is_equality) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!bool',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($non_bool_types) {
- return new Type\Union($non_bool_types);
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileNonEmptyCountable(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality,
- ?int $min_count
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $existing_var_atomic_types = $existing_var_type->getAtomicTypes();
-
- if (isset($existing_var_atomic_types['array'])) {
- $array_atomic_type = $existing_var_atomic_types['array'];
- $did_remove_type = false;
-
- if (($array_atomic_type instanceof Type\Atomic\TNonEmptyArray
- || $array_atomic_type instanceof Type\Atomic\TNonEmptyList)
- && ($min_count === null
- || $array_atomic_type->count >= $min_count)
- ) {
- $did_remove_type = true;
-
- $existing_var_type->removeType('array');
- } elseif ($array_atomic_type->getId() !== 'array') {
- $did_remove_type = true;
-
- $existing_var_type->addType(new TArray(
- [
- new Type\Union([new TEmpty]),
- new Type\Union([new TEmpty]),
- ]
- ));
- } elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
- $did_remove_type = true;
-
- foreach ($array_atomic_type->properties as $property_type) {
- if (!$property_type->possibly_undefined) {
- $did_remove_type = false;
- break;
- }
- }
- }
-
- if (!$is_equality
- && !$existing_var_type->hasMixed()
- && (!$did_remove_type || empty($existing_var_type->getAtomicTypes()))
- ) {
- if ($key && $code_location) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!non-empty-countable',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
- }
- }
-
- return $existing_var_type;
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileNull(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $did_remove_type = false;
-
- if ($existing_var_type->hasType('null')) {
- $did_remove_type = true;
- $existing_var_type->removeType('null');
- }
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if ($type->as->hasType('null') && !$type->as->isNull()) {
- $type->as->removeType('null');
-
- $did_remove_type = true;
-
- $existing_var_type->bustCache();
- }
- }
- }
-
- if (!$did_remove_type || empty($existing_var_type->getAtomicTypes())) {
- if ($key && $code_location && !$is_equality) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!null',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($existing_var_type->getAtomicTypes()) {
- return $existing_var_type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileFalsyOrEmpty(
- string $assertion,
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality,
- bool $is_strict_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $existing_var_atomic_types = $existing_var_type->getAtomicTypes();
-
- $did_remove_type = $existing_var_type->hasDefinitelyNumericType(false)
- || $existing_var_type->isEmpty()
- || $existing_var_type->hasType('bool')
- || $existing_var_type->possibly_undefined
- || $existing_var_type->possibly_undefined_from_try
- || $existing_var_type->hasType('iterable');
-
- if ($is_strict_equality && $assertion === 'empty') {
- $existing_var_type->removeType('null');
- $existing_var_type->removeType('false');
-
- if ($existing_var_type->hasType('array')
- && $existing_var_type->getAtomicTypes()['array']->getId() === 'array'
- ) {
- $existing_var_type->removeType('array');
- }
-
- if ($existing_var_type->hasMixed()) {
- $existing_var_type->removeType('mixed');
-
- if (!$existing_var_atomic_types['mixed'] instanceof Type\Atomic\TEmptyMixed) {
- $existing_var_type->addType(new Type\Atomic\TNonEmptyMixed);
- }
- }
-
- if ($existing_var_type->hasScalar()) {
- $existing_var_type->removeType('scalar');
-
- if (!$existing_var_atomic_types['scalar'] instanceof Type\Atomic\TEmptyScalar) {
- $existing_var_type->addType(new Type\Atomic\TNonEmptyScalar);
- }
- }
-
- if (isset($existing_var_atomic_types['string'])) {
- $existing_var_type->removeType('string');
-
- $existing_var_type->addType(new Type\Atomic\TNonEmptyString);
- }
-
- self::removeFalsyNegatedLiteralTypes(
- $existing_var_type,
- $did_remove_type
- );
-
- $existing_var_type->possibly_undefined = false;
- $existing_var_type->possibly_undefined_from_try = false;
-
- if ($existing_var_type->getAtomicTypes()) {
- return $existing_var_type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- if ($existing_var_type->hasMixed()) {
- if ($existing_var_type->isMixed()
- && $existing_var_atomic_types['mixed'] instanceof Type\Atomic\TEmptyMixed
- ) {
- if ($code_location
- && $key
- && IssueBuffer::accepts(
- new ParadoxicalCondition(
- 'Found a paradox when evaluating ' . $key
- . ' of type ' . $existing_var_type->getId()
- . ' and trying to reconcile it with a non-' . $assertion . ' assertion',
- $code_location
- ),
- $suppressed_issues
- )
- ) {
- // fall through
- }
-
- return Type::getMixed();
- }
-
- if (!$existing_var_atomic_types['mixed'] instanceof Type\Atomic\TNonEmptyMixed) {
- $did_remove_type = true;
- $existing_var_type->removeType('mixed');
-
- if (!$existing_var_atomic_types['mixed'] instanceof Type\Atomic\TEmptyMixed) {
- $existing_var_type->addType(new Type\Atomic\TNonEmptyMixed);
- }
- } elseif ($existing_var_type->isMixed() && !$is_equality) {
- if ($code_location
- && $key
- && IssueBuffer::accepts(
- new RedundantCondition(
- 'Found a redundant condition when evaluating ' . $key
- . ' of type ' . $existing_var_type->getId()
- . ' and trying to reconcile it with a non-' . $assertion . ' assertion',
- $code_location
- ),
- $suppressed_issues
- )
- ) {
- // fall through
- }
- }
-
- if ($existing_var_type->isMixed()) {
- return $existing_var_type;
- }
- }
-
- if ($existing_var_type->hasScalar()) {
- if (!$existing_var_atomic_types['scalar'] instanceof Type\Atomic\TNonEmptyScalar) {
- $did_remove_type = true;
- $existing_var_type->removeType('scalar');
-
- if (!$existing_var_atomic_types['scalar'] instanceof Type\Atomic\TEmptyScalar) {
- $existing_var_type->addType(new Type\Atomic\TNonEmptyScalar);
- }
- } elseif ($existing_var_type->isSingle() && !$is_equality) {
- if ($code_location
- && $key
- && IssueBuffer::accepts(
- new RedundantCondition(
- 'Found a redundant condition when evaluating ' . $key
- . ' of type ' . $existing_var_type->getId()
- . ' and trying to reconcile it with a non-' . $assertion . ' assertion',
- $code_location
- ),
- $suppressed_issues
- )
- ) {
- // fall through
- }
- }
-
- if ($existing_var_type->isSingle()) {
- return $existing_var_type;
- }
- }
-
- if (isset($existing_var_atomic_types['string'])) {
- if (!$existing_var_atomic_types['string'] instanceof Type\Atomic\TNonEmptyString) {
- $did_remove_type = true;
- if (!$existing_var_atomic_types['string'] instanceof Type\Atomic\TLowercaseString) {
- $existing_var_type->removeType('string');
-
- $existing_var_type->addType(new Type\Atomic\TNonEmptyString);
- }
- } elseif ($existing_var_type->isSingle() && !$is_equality) {
- if ($code_location
- && $key
- && IssueBuffer::accepts(
- new RedundantCondition(
- 'Found a redundant condition when evaluating ' . $key
- . ' of type ' . $existing_var_type->getId()
- . ' and trying to reconcile it with a non-' . $assertion . ' assertion',
- $code_location
- ),
- $suppressed_issues
- )
- ) {
- // fall through
- }
- }
-
- if ($existing_var_type->isSingle()) {
- return $existing_var_type;
- }
- }
-
- if ($existing_var_type->hasType('null')) {
- $did_remove_type = true;
- $existing_var_type->removeType('null');
- }
-
- if ($existing_var_type->hasType('false')) {
- $did_remove_type = true;
- $existing_var_type->removeType('false');
- }
-
- if ($existing_var_type->hasType('bool')) {
- $did_remove_type = true;
- $existing_var_type->removeType('bool');
- $existing_var_type->addType(new TTrue);
- }
-
- self::removeFalsyNegatedLiteralTypes(
- $existing_var_type,
- $did_remove_type
- );
-
- $existing_var_type->possibly_undefined = false;
- $existing_var_type->possibly_undefined_from_try = false;
-
- if ((!$did_remove_type || empty($existing_var_type->getAtomicTypes())) && !$existing_var_type->hasTemplate()) {
- if ($key && $code_location && !$is_equality) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!' . $assertion,
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($existing_var_type->getAtomicTypes()) {
- return $existing_var_type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getEmpty();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileScalar(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $non_scalar_types = [];
- $did_remove_type = false;
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if (!$type->as->hasScalar() || $is_equality) {
- $non_scalar_types[] = $type;
- }
-
- $did_remove_type = true;
- } elseif (!($type instanceof Scalar)) {
- $non_scalar_types[] = $type;
- } else {
- $did_remove_type = true;
-
- if ($is_equality) {
- $non_scalar_types[] = $type;
- }
- }
- }
-
- if (!$did_remove_type || !$non_scalar_types) {
- if ($key && $code_location) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!scalar',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($non_scalar_types) {
- $type = new Type\Union($non_scalar_types);
- $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
- $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
- $type->from_docblock = $existing_var_type->from_docblock;
- return $type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileObject(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $non_object_types = [];
- $did_remove_type = false;
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if (!$type->as->hasObject() || $is_equality) {
- $non_object_types[] = $type;
- }
-
- $did_remove_type = true;
- } elseif ($type instanceof TCallable) {
- $non_object_types[] = new Atomic\TCallableArray([
- Type::getArrayKey(),
- Type::getMixed()
- ]);
- $non_object_types[] = new Atomic\TCallableString();
- $did_remove_type = true;
- } elseif (!$type->isObjectType()) {
- $non_object_types[] = $type;
- } else {
- $did_remove_type = true;
-
- if ($is_equality) {
- $non_object_types[] = $type;
- }
- }
- }
-
- if (!$non_object_types || !$did_remove_type) {
- if ($key && $code_location) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!object',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($non_object_types) {
- $type = new Type\Union($non_object_types);
- $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
- $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
- $type->from_docblock = $existing_var_type->from_docblock;
- return $type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileNumeric(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $non_numeric_types = [];
- $did_remove_type = $existing_var_type->hasString()
- || $existing_var_type->hasScalar();
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if (!$type->as->hasNumeric() || $is_equality) {
- if ($type->as->hasMixed()) {
- $did_remove_type = true;
- }
-
- $non_numeric_types[] = $type;
- }
- } elseif ($type instanceof TArrayKey) {
- $did_remove_type = true;
- $non_numeric_types[] = new TString();
- } elseif (!$type->isNumericType()) {
- $non_numeric_types[] = $type;
- } else {
- $did_remove_type = true;
-
- if ($is_equality) {
- $non_numeric_types[] = $type;
- }
- }
- }
-
- if (!$non_numeric_types || !$did_remove_type) {
- if ($key && $code_location && !$is_equality) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!numeric',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($non_numeric_types) {
- $type = new Type\Union($non_numeric_types);
- $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
- $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
- $type->from_docblock = $existing_var_type->from_docblock;
- return $type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileInt(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $non_int_types = [];
- $did_remove_type = false;
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if (!$type->as->hasInt() || $is_equality) {
- if ($type->as->hasMixed()) {
- $did_remove_type = true;
- }
-
- $non_int_types[] = $type;
- }
- } elseif ($type instanceof TArrayKey) {
- $did_remove_type = true;
- $non_int_types[] = new TString();
- } elseif ($type instanceof TScalar) {
- $did_remove_type = true;
- $non_int_types[] = new TString();
- $non_int_types[] = new TFloat();
- $non_int_types[] = new TBool();
- } elseif ($type instanceof TInt) {
- $did_remove_type = true;
-
- if ($is_equality) {
- $non_int_types[] = $type;
- } elseif ($existing_var_type->from_calculation) {
- $non_int_types[] = new TFloat();
- }
- } else {
- $non_int_types[] = $type;
- }
- }
-
- if (!$non_int_types || !$did_remove_type) {
- if ($key && $code_location && !$is_equality) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!int',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($non_int_types) {
- $type = new Type\Union($non_int_types);
- $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
- $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
- $type->from_docblock = $existing_var_type->from_docblock;
- return $type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileFloat(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $non_float_types = [];
- $did_remove_type = false;
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if (!$type->as->hasFloat() || $is_equality) {
- if ($type->as->hasMixed()) {
- $did_remove_type = true;
- }
-
- $non_float_types[] = $type;
- }
- } elseif ($type instanceof TScalar) {
- $did_remove_type = true;
- $non_float_types[] = new TString();
- $non_float_types[] = new TInt();
- $non_float_types[] = new TBool();
- } elseif ($type instanceof TFloat) {
- $did_remove_type = true;
-
- if ($is_equality) {
- $non_float_types[] = $type;
- }
- } else {
- $non_float_types[] = $type;
- }
- }
-
- if (!$non_float_types || !$did_remove_type) {
- if ($key && $code_location && !$is_equality) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!float',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($non_float_types) {
- $type = new Type\Union($non_float_types);
- $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
- $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
- $type->from_docblock = $existing_var_type->from_docblock;
- return $type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileString(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $non_string_types = [];
- $did_remove_type = $existing_var_type->hasScalar();
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if (!$type->as->hasString() || $is_equality) {
- if ($type->as->hasMixed()) {
- $did_remove_type = true;
- }
-
- $non_string_types[] = $type;
- }
- } elseif ($type instanceof TArrayKey) {
- $non_string_types[] = new TInt();
- $did_remove_type = true;
- } elseif ($type instanceof TCallable) {
- $non_string_types[] = new Atomic\TCallableArray([
- Type::getArrayKey(),
- Type::getMixed()
- ]);
- $non_string_types[] = new Atomic\TCallableObject();
- $did_remove_type = true;
- } elseif ($type instanceof TNumeric) {
- $non_string_types[] = $type;
- $did_remove_type = true;
- } elseif ($type instanceof TScalar) {
- $did_remove_type = true;
- $non_string_types[] = new TFloat();
- $non_string_types[] = new TInt();
- $non_string_types[] = new TBool();
- } elseif (!$type instanceof TString) {
- $non_string_types[] = $type;
- } else {
- $did_remove_type = true;
-
- if ($is_equality) {
- $non_string_types[] = $type;
- }
- }
- }
-
- if (!$non_string_types || !$did_remove_type) {
- if ($key && $code_location) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!string',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($non_string_types) {
- $type = new Type\Union($non_string_types);
- $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
- $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
- $type->from_docblock = $existing_var_type->from_docblock;
- return $type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @param string[] $suppressed_issues
- * @param 0|1|2 $failed_reconciliation
- */
- private static function reconcileArray(
- Type\Union $existing_var_type,
- ?string $key,
- ?CodeLocation $code_location,
- array $suppressed_issues,
- int &$failed_reconciliation,
- bool $is_equality
- ) : Type\Union {
- $old_var_type_string = $existing_var_type->getId();
- $non_array_types = [];
- $did_remove_type = $existing_var_type->hasScalar();
-
- foreach ($existing_var_type->getAtomicTypes() as $type) {
- if ($type instanceof TTemplateParam) {
- if (!$type->as->hasArray() || $is_equality) {
- if ($type->as->hasMixed()) {
- $did_remove_type = true;
- }
-
- $non_array_types[] = $type;
- }
- } elseif ($type instanceof TCallable) {
- $non_array_types[] = new Atomic\TCallableString();
- $non_array_types[] = new Atomic\TCallableObject();
- $did_remove_type = true;
- } elseif ($type instanceof Atomic\TIterable) {
- if (!$type->type_params[0]->isMixed() || !$type->type_params[1]->isMixed()) {
- $non_array_types[] = new Atomic\TGenericObject('Traversable', $type->type_params);
- } else {
- $non_array_types[] = new TNamedObject('Traversable');
- }
-
- $did_remove_type = true;
- } elseif (!$type instanceof TArray
- && !$type instanceof ObjectLike
- && !$type instanceof Atomic\TList
- ) {
- $non_array_types[] = $type;
- } else {
- $did_remove_type = true;
-
- if ($is_equality) {
- $non_array_types[] = $type;
- }
- }
- }
-
- if ((!$non_array_types || !$did_remove_type)) {
- if ($key && $code_location) {
- self::triggerIssueForImpossible(
- $existing_var_type,
- $old_var_type_string,
- $key,
- '!array',
- !$did_remove_type,
- $code_location,
- $suppressed_issues
- );
- }
-
- if (!$did_remove_type) {
- $failed_reconciliation = 1;
- }
- }
-
- if ($non_array_types) {
- $type = new Type\Union($non_array_types);
- $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
- $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
- $type->from_docblock = $existing_var_type->from_docblock;
- return $type;
- }
-
- $failed_reconciliation = 2;
-
- return Type::getMixed();
- }
-
- /**
- * @return void
- */
- private static function removeFalsyNegatedLiteralTypes(
- Type\Union $existing_var_type,
- bool &$did_remove_type
- ) {
- if ($existing_var_type->hasString()) {
- $existing_string_types = $existing_var_type->getLiteralStrings();
-
- if ($existing_string_types) {
- foreach ($existing_string_types as $string_key => $literal_type) {
- if (!$literal_type->value) {
- $existing_var_type->removeType($string_key);
- $did_remove_type = true;
- }
- }
- } else {
- $did_remove_type = true;
- }
- }
-
- if ($existing_var_type->hasInt()) {
- $existing_int_types = $existing_var_type->getLiteralInts();
-
- if ($existing_int_types) {
- foreach ($existing_int_types as $int_key => $literal_type) {
- if (!$literal_type->value) {
- $existing_var_type->removeType($int_key);
- $did_remove_type = true;
- }
- }
- } else {
- $did_remove_type = true;
- }
- }
-
- if ($existing_var_type->hasType('array')) {
- $array_atomic_type = $existing_var_type->getAtomicTypes()['array'];
-
- if ($array_atomic_type instanceof Type\Atomic\TArray
- && !$array_atomic_type instanceof Type\Atomic\TNonEmptyArray
- ) {
- $did_remove_type = true;
-
- if ($array_atomic_type->getId() === 'array') {
- $existing_var_type->removeType('array');
- } else {
- $existing_var_type->addType(
- new Type\Atomic\TNonEmptyArray(
- $array_atomic_type->type_params
- )
- );
- }
- } elseif ($array_atomic_type instanceof Type\Atomic\TList
- && !$array_atomic_type instanceof Type\Atomic\TNonEmptyList
- ) {
- $did_remove_type = true;
-
- $existing_var_type->addType(
- new Type\Atomic\TNonEmptyList(
- $array_atomic_type->type_param
- )
- );
- } elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike
- && !$array_atomic_type->sealed
- ) {
- $did_remove_type = true;
- }
- }
- }
-
/**
* @param string $assertion
* @param int $bracket_pos
diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php
new file mode 100644
index 000000000..d2a8d9e46
--- /dev/null
+++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php
@@ -0,0 +1,1255 @@
+> $template_type_map
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ *
+ * @return Type\Union
+ */
+ public static function reconcile(
+ string $assertion,
+ Type\Union $existing_var_type,
+ ?string $key = null,
+ ?CodeLocation $code_location = null,
+ array $suppressed_issues = [],
+ int &$failed_reconciliation = 0,
+ bool $is_equality = false,
+ bool $is_strict_equality = false
+ ) : ?Type\Union {
+ if ($assertion === 'object' && !$existing_var_type->hasMixed()) {
+ return self::reconcileObject(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'scalar' && !$existing_var_type->hasMixed()) {
+ return self::reconcileScalar(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'bool' && !$existing_var_type->hasMixed()) {
+ return self::reconcileBool(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'numeric' && !$existing_var_type->hasMixed()) {
+ return self::reconcileNumeric(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'float' && !$existing_var_type->hasMixed()) {
+ return self::reconcileFloat(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'int' && !$existing_var_type->hasMixed()) {
+ return self::reconcileInt(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'string' && !$existing_var_type->hasMixed()) {
+ return self::reconcileString(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'array' && !$existing_var_type->hasMixed()) {
+ return self::reconcileArray(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'falsy' || $assertion === 'empty') {
+ return self::reconcileFalsyOrEmpty(
+ $assertion,
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality,
+ $is_strict_equality
+ );
+ }
+
+ if ($assertion === 'null' && !$existing_var_type->hasMixed()) {
+ return self::reconcileNull(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality
+ );
+ }
+
+ if ($assertion === 'non-empty-countable') {
+ return self::reconcileNonEmptyCountable(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality,
+ null
+ );
+ }
+
+ if ($assertion === 'callable') {
+ return self::reconcileCallable(
+ $existing_var_type
+ );
+ }
+
+ if (substr($assertion, 0, 13) === 'has-at-least-') {
+ return self::reconcileNonEmptyCountable(
+ $existing_var_type,
+ $key,
+ $code_location,
+ $suppressed_issues,
+ $failed_reconciliation,
+ $is_equality,
+ (int) substr($assertion, 13)
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileCallable(
+ Type\Union $existing_var_type
+ ) : Type\Union {
+ foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $type) {
+ if ($type instanceof Type\Atomic\TLiteralString
+ && \Psalm\Internal\Codebase\CallMap::inCallMap($type->value)
+ ) {
+ $existing_var_type->removeType($atomic_key);
+ }
+
+ if ($type->isCallableType()) {
+ $existing_var_type->removeType($atomic_key);
+ }
+ }
+
+ return $existing_var_type;
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileBool(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $non_bool_types = [];
+ $did_remove_type = false;
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if (!$type->as->hasBool()) {
+ $non_bool_types[] = $type;
+ }
+
+ $did_remove_type = true;
+ } elseif (!$type instanceof TBool
+ || ($is_equality && get_class($type) === TBool::class)
+ ) {
+ if ($type instanceof TScalar) {
+ $did_remove_type = true;
+ $non_bool_types[] = new TString();
+ $non_bool_types[] = new TInt();
+ $non_bool_types[] = new TFloat();
+ } else {
+ $non_bool_types[] = $type;
+ }
+ } else {
+ $did_remove_type = true;
+ }
+ }
+
+ if (!$did_remove_type || !$non_bool_types) {
+ if ($key && $code_location && !$is_equality) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!bool',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($non_bool_types) {
+ return new Type\Union($non_bool_types);
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileNonEmptyCountable(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality,
+ ?int $min_count
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $existing_var_atomic_types = $existing_var_type->getAtomicTypes();
+
+ if (isset($existing_var_atomic_types['array'])) {
+ $array_atomic_type = $existing_var_atomic_types['array'];
+ $did_remove_type = false;
+
+ if (($array_atomic_type instanceof Type\Atomic\TNonEmptyArray
+ || $array_atomic_type instanceof Type\Atomic\TNonEmptyList)
+ && ($min_count === null
+ || $array_atomic_type->count >= $min_count)
+ ) {
+ $did_remove_type = true;
+
+ $existing_var_type->removeType('array');
+ } elseif ($array_atomic_type->getId() !== 'array') {
+ $did_remove_type = true;
+
+ $existing_var_type->addType(new TArray(
+ [
+ new Type\Union([new TEmpty]),
+ new Type\Union([new TEmpty]),
+ ]
+ ));
+ } elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
+ $did_remove_type = true;
+
+ foreach ($array_atomic_type->properties as $property_type) {
+ if (!$property_type->possibly_undefined) {
+ $did_remove_type = false;
+ break;
+ }
+ }
+ }
+
+ if (!$is_equality
+ && !$existing_var_type->hasMixed()
+ && (!$did_remove_type || empty($existing_var_type->getAtomicTypes()))
+ ) {
+ if ($key && $code_location) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!non-empty-countable',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+ }
+ }
+
+ return $existing_var_type;
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileNull(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $did_remove_type = false;
+
+ if ($existing_var_type->hasType('null')) {
+ $did_remove_type = true;
+ $existing_var_type->removeType('null');
+ }
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if ($type->as->hasType('null') && !$type->as->isNull()) {
+ $type->as->removeType('null');
+
+ $did_remove_type = true;
+
+ $existing_var_type->bustCache();
+ }
+ }
+ }
+
+ if (!$did_remove_type || empty($existing_var_type->getAtomicTypes())) {
+ if ($key && $code_location && !$is_equality) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!null',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($existing_var_type->getAtomicTypes()) {
+ return $existing_var_type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileFalsyOrEmpty(
+ string $assertion,
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality,
+ bool $is_strict_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $existing_var_atomic_types = $existing_var_type->getAtomicTypes();
+
+ $did_remove_type = $existing_var_type->hasDefinitelyNumericType(false)
+ || $existing_var_type->isEmpty()
+ || $existing_var_type->hasType('bool')
+ || $existing_var_type->possibly_undefined
+ || $existing_var_type->possibly_undefined_from_try
+ || $existing_var_type->hasType('iterable');
+
+ if ($is_strict_equality && $assertion === 'empty') {
+ $existing_var_type->removeType('null');
+ $existing_var_type->removeType('false');
+
+ if ($existing_var_type->hasType('array')
+ && $existing_var_type->getAtomicTypes()['array']->getId() === 'array'
+ ) {
+ $existing_var_type->removeType('array');
+ }
+
+ if ($existing_var_type->hasMixed()) {
+ $existing_var_type->removeType('mixed');
+
+ if (!$existing_var_atomic_types['mixed'] instanceof Type\Atomic\TEmptyMixed) {
+ $existing_var_type->addType(new Type\Atomic\TNonEmptyMixed);
+ }
+ }
+
+ if ($existing_var_type->hasScalar()) {
+ $existing_var_type->removeType('scalar');
+
+ if (!$existing_var_atomic_types['scalar'] instanceof Type\Atomic\TEmptyScalar) {
+ $existing_var_type->addType(new Type\Atomic\TNonEmptyScalar);
+ }
+ }
+
+ if (isset($existing_var_atomic_types['string'])) {
+ $existing_var_type->removeType('string');
+
+ $existing_var_type->addType(new Type\Atomic\TNonEmptyString);
+ }
+
+ self::removeFalsyNegatedLiteralTypes(
+ $existing_var_type,
+ $did_remove_type
+ );
+
+ $existing_var_type->possibly_undefined = false;
+ $existing_var_type->possibly_undefined_from_try = false;
+
+ if ($existing_var_type->getAtomicTypes()) {
+ return $existing_var_type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ if ($existing_var_type->hasMixed()) {
+ if ($existing_var_type->isMixed()
+ && $existing_var_atomic_types['mixed'] instanceof Type\Atomic\TEmptyMixed
+ ) {
+ if ($code_location
+ && $key
+ && IssueBuffer::accepts(
+ new ParadoxicalCondition(
+ 'Found a paradox when evaluating ' . $key
+ . ' of type ' . $existing_var_type->getId()
+ . ' and trying to reconcile it with a non-' . $assertion . ' assertion',
+ $code_location
+ ),
+ $suppressed_issues
+ )
+ ) {
+ // fall through
+ }
+
+ return Type::getMixed();
+ }
+
+ if (!$existing_var_atomic_types['mixed'] instanceof Type\Atomic\TNonEmptyMixed) {
+ $did_remove_type = true;
+ $existing_var_type->removeType('mixed');
+
+ if (!$existing_var_atomic_types['mixed'] instanceof Type\Atomic\TEmptyMixed) {
+ $existing_var_type->addType(new Type\Atomic\TNonEmptyMixed);
+ }
+ } elseif ($existing_var_type->isMixed() && !$is_equality) {
+ if ($code_location
+ && $key
+ && IssueBuffer::accepts(
+ new RedundantCondition(
+ 'Found a redundant condition when evaluating ' . $key
+ . ' of type ' . $existing_var_type->getId()
+ . ' and trying to reconcile it with a non-' . $assertion . ' assertion',
+ $code_location
+ ),
+ $suppressed_issues
+ )
+ ) {
+ // fall through
+ }
+ }
+
+ if ($existing_var_type->isMixed()) {
+ return $existing_var_type;
+ }
+ }
+
+ if ($existing_var_type->hasScalar()) {
+ if (!$existing_var_atomic_types['scalar'] instanceof Type\Atomic\TNonEmptyScalar) {
+ $did_remove_type = true;
+ $existing_var_type->removeType('scalar');
+
+ if (!$existing_var_atomic_types['scalar'] instanceof Type\Atomic\TEmptyScalar) {
+ $existing_var_type->addType(new Type\Atomic\TNonEmptyScalar);
+ }
+ } elseif ($existing_var_type->isSingle() && !$is_equality) {
+ if ($code_location
+ && $key
+ && IssueBuffer::accepts(
+ new RedundantCondition(
+ 'Found a redundant condition when evaluating ' . $key
+ . ' of type ' . $existing_var_type->getId()
+ . ' and trying to reconcile it with a non-' . $assertion . ' assertion',
+ $code_location
+ ),
+ $suppressed_issues
+ )
+ ) {
+ // fall through
+ }
+ }
+
+ if ($existing_var_type->isSingle()) {
+ return $existing_var_type;
+ }
+ }
+
+ if (isset($existing_var_atomic_types['string'])) {
+ if (!$existing_var_atomic_types['string'] instanceof Type\Atomic\TNonEmptyString) {
+ $did_remove_type = true;
+ if (!$existing_var_atomic_types['string'] instanceof Type\Atomic\TLowercaseString) {
+ $existing_var_type->removeType('string');
+
+ $existing_var_type->addType(new Type\Atomic\TNonEmptyString);
+ }
+ } elseif ($existing_var_type->isSingle() && !$is_equality) {
+ if ($code_location
+ && $key
+ && IssueBuffer::accepts(
+ new RedundantCondition(
+ 'Found a redundant condition when evaluating ' . $key
+ . ' of type ' . $existing_var_type->getId()
+ . ' and trying to reconcile it with a non-' . $assertion . ' assertion',
+ $code_location
+ ),
+ $suppressed_issues
+ )
+ ) {
+ // fall through
+ }
+ }
+
+ if ($existing_var_type->isSingle()) {
+ return $existing_var_type;
+ }
+ }
+
+ if ($existing_var_type->hasType('null')) {
+ $did_remove_type = true;
+ $existing_var_type->removeType('null');
+ }
+
+ if ($existing_var_type->hasType('false')) {
+ $did_remove_type = true;
+ $existing_var_type->removeType('false');
+ }
+
+ if ($existing_var_type->hasType('bool')) {
+ $did_remove_type = true;
+ $existing_var_type->removeType('bool');
+ $existing_var_type->addType(new TTrue);
+ }
+
+ self::removeFalsyNegatedLiteralTypes(
+ $existing_var_type,
+ $did_remove_type
+ );
+
+ $existing_var_type->possibly_undefined = false;
+ $existing_var_type->possibly_undefined_from_try = false;
+
+ if ((!$did_remove_type || empty($existing_var_type->getAtomicTypes())) && !$existing_var_type->hasTemplate()) {
+ if ($key && $code_location && !$is_equality) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!' . $assertion,
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($existing_var_type->getAtomicTypes()) {
+ return $existing_var_type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getEmpty();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileScalar(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $non_scalar_types = [];
+ $did_remove_type = false;
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if (!$type->as->hasScalar() || $is_equality) {
+ $non_scalar_types[] = $type;
+ }
+
+ $did_remove_type = true;
+ } elseif (!($type instanceof Scalar)) {
+ $non_scalar_types[] = $type;
+ } else {
+ $did_remove_type = true;
+
+ if ($is_equality) {
+ $non_scalar_types[] = $type;
+ }
+ }
+ }
+
+ if (!$did_remove_type || !$non_scalar_types) {
+ if ($key && $code_location) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!scalar',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($non_scalar_types) {
+ $type = new Type\Union($non_scalar_types);
+ $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
+ $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
+ $type->from_docblock = $existing_var_type->from_docblock;
+ return $type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileObject(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $non_object_types = [];
+ $did_remove_type = false;
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if (!$type->as->hasObject() || $is_equality) {
+ $non_object_types[] = $type;
+ }
+
+ $did_remove_type = true;
+ } elseif ($type instanceof TCallable) {
+ $non_object_types[] = new Atomic\TCallableArray([
+ Type::getArrayKey(),
+ Type::getMixed()
+ ]);
+ $non_object_types[] = new Atomic\TCallableString();
+ $did_remove_type = true;
+ } elseif (!$type->isObjectType()) {
+ $non_object_types[] = $type;
+ } else {
+ $did_remove_type = true;
+
+ if ($is_equality) {
+ $non_object_types[] = $type;
+ }
+ }
+ }
+
+ if (!$non_object_types || !$did_remove_type) {
+ if ($key && $code_location) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!object',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($non_object_types) {
+ $type = new Type\Union($non_object_types);
+ $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
+ $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
+ $type->from_docblock = $existing_var_type->from_docblock;
+ return $type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileNumeric(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $non_numeric_types = [];
+ $did_remove_type = $existing_var_type->hasString()
+ || $existing_var_type->hasScalar();
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if (!$type->as->hasNumeric() || $is_equality) {
+ if ($type->as->hasMixed()) {
+ $did_remove_type = true;
+ }
+
+ $non_numeric_types[] = $type;
+ }
+ } elseif ($type instanceof TArrayKey) {
+ $did_remove_type = true;
+ $non_numeric_types[] = new TString();
+ } elseif (!$type->isNumericType()) {
+ $non_numeric_types[] = $type;
+ } else {
+ $did_remove_type = true;
+
+ if ($is_equality) {
+ $non_numeric_types[] = $type;
+ }
+ }
+ }
+
+ if (!$non_numeric_types || !$did_remove_type) {
+ if ($key && $code_location && !$is_equality) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!numeric',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($non_numeric_types) {
+ $type = new Type\Union($non_numeric_types);
+ $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
+ $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
+ $type->from_docblock = $existing_var_type->from_docblock;
+ return $type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileInt(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $non_int_types = [];
+ $did_remove_type = false;
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if (!$type->as->hasInt() || $is_equality) {
+ if ($type->as->hasMixed()) {
+ $did_remove_type = true;
+ }
+
+ $non_int_types[] = $type;
+ }
+ } elseif ($type instanceof TArrayKey) {
+ $did_remove_type = true;
+ $non_int_types[] = new TString();
+ } elseif ($type instanceof TScalar) {
+ $did_remove_type = true;
+ $non_int_types[] = new TString();
+ $non_int_types[] = new TFloat();
+ $non_int_types[] = new TBool();
+ } elseif ($type instanceof TInt) {
+ $did_remove_type = true;
+
+ if ($is_equality) {
+ $non_int_types[] = $type;
+ } elseif ($existing_var_type->from_calculation) {
+ $non_int_types[] = new TFloat();
+ }
+ } else {
+ $non_int_types[] = $type;
+ }
+ }
+
+ if (!$non_int_types || !$did_remove_type) {
+ if ($key && $code_location && !$is_equality) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!int',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($non_int_types) {
+ $type = new Type\Union($non_int_types);
+ $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
+ $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
+ $type->from_docblock = $existing_var_type->from_docblock;
+ return $type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileFloat(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $non_float_types = [];
+ $did_remove_type = false;
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if (!$type->as->hasFloat() || $is_equality) {
+ if ($type->as->hasMixed()) {
+ $did_remove_type = true;
+ }
+
+ $non_float_types[] = $type;
+ }
+ } elseif ($type instanceof TScalar) {
+ $did_remove_type = true;
+ $non_float_types[] = new TString();
+ $non_float_types[] = new TInt();
+ $non_float_types[] = new TBool();
+ } elseif ($type instanceof TFloat) {
+ $did_remove_type = true;
+
+ if ($is_equality) {
+ $non_float_types[] = $type;
+ }
+ } else {
+ $non_float_types[] = $type;
+ }
+ }
+
+ if (!$non_float_types || !$did_remove_type) {
+ if ($key && $code_location && !$is_equality) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!float',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($non_float_types) {
+ $type = new Type\Union($non_float_types);
+ $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
+ $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
+ $type->from_docblock = $existing_var_type->from_docblock;
+ return $type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileString(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $non_string_types = [];
+ $did_remove_type = $existing_var_type->hasScalar();
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if (!$type->as->hasString() || $is_equality) {
+ if ($type->as->hasMixed()) {
+ $did_remove_type = true;
+ }
+
+ $non_string_types[] = $type;
+ }
+ } elseif ($type instanceof TArrayKey) {
+ $non_string_types[] = new TInt();
+ $did_remove_type = true;
+ } elseif ($type instanceof TCallable) {
+ $non_string_types[] = new Atomic\TCallableArray([
+ Type::getArrayKey(),
+ Type::getMixed()
+ ]);
+ $non_string_types[] = new Atomic\TCallableObject();
+ $did_remove_type = true;
+ } elseif ($type instanceof TNumeric) {
+ $non_string_types[] = $type;
+ $did_remove_type = true;
+ } elseif ($type instanceof TScalar) {
+ $did_remove_type = true;
+ $non_string_types[] = new TFloat();
+ $non_string_types[] = new TInt();
+ $non_string_types[] = new TBool();
+ } elseif (!$type instanceof TString) {
+ $non_string_types[] = $type;
+ } else {
+ $did_remove_type = true;
+
+ if ($is_equality) {
+ $non_string_types[] = $type;
+ }
+ }
+ }
+
+ if (!$non_string_types || !$did_remove_type) {
+ if ($key && $code_location) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!string',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($non_string_types) {
+ $type = new Type\Union($non_string_types);
+ $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
+ $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
+ $type->from_docblock = $existing_var_type->from_docblock;
+ return $type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @param string[] $suppressed_issues
+ * @param 0|1|2 $failed_reconciliation
+ */
+ private static function reconcileArray(
+ Type\Union $existing_var_type,
+ ?string $key,
+ ?CodeLocation $code_location,
+ array $suppressed_issues,
+ int &$failed_reconciliation,
+ bool $is_equality
+ ) : Type\Union {
+ $old_var_type_string = $existing_var_type->getId();
+ $non_array_types = [];
+ $did_remove_type = $existing_var_type->hasScalar();
+
+ foreach ($existing_var_type->getAtomicTypes() as $type) {
+ if ($type instanceof TTemplateParam) {
+ if (!$type->as->hasArray() || $is_equality) {
+ if ($type->as->hasMixed()) {
+ $did_remove_type = true;
+ }
+
+ $non_array_types[] = $type;
+ }
+ } elseif ($type instanceof TCallable) {
+ $non_array_types[] = new Atomic\TCallableString();
+ $non_array_types[] = new Atomic\TCallableObject();
+ $did_remove_type = true;
+ } elseif ($type instanceof Atomic\TIterable) {
+ if (!$type->type_params[0]->isMixed() || !$type->type_params[1]->isMixed()) {
+ $non_array_types[] = new Atomic\TGenericObject('Traversable', $type->type_params);
+ } else {
+ $non_array_types[] = new TNamedObject('Traversable');
+ }
+
+ $did_remove_type = true;
+ } elseif (!$type instanceof TArray
+ && !$type instanceof ObjectLike
+ && !$type instanceof Atomic\TList
+ ) {
+ $non_array_types[] = $type;
+ } else {
+ $did_remove_type = true;
+
+ if ($is_equality) {
+ $non_array_types[] = $type;
+ }
+ }
+ }
+
+ if ((!$non_array_types || !$did_remove_type)) {
+ if ($key && $code_location) {
+ self::triggerIssueForImpossible(
+ $existing_var_type,
+ $old_var_type_string,
+ $key,
+ '!array',
+ !$did_remove_type,
+ $code_location,
+ $suppressed_issues
+ );
+ }
+
+ if (!$did_remove_type) {
+ $failed_reconciliation = 1;
+ }
+ }
+
+ if ($non_array_types) {
+ $type = new Type\Union($non_array_types);
+ $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues;
+ $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues;
+ $type->from_docblock = $existing_var_type->from_docblock;
+ return $type;
+ }
+
+ $failed_reconciliation = 2;
+
+ return Type::getMixed();
+ }
+
+ /**
+ * @return void
+ */
+ private static function removeFalsyNegatedLiteralTypes(
+ Type\Union $existing_var_type,
+ bool &$did_remove_type
+ ) {
+ if ($existing_var_type->hasString()) {
+ $existing_string_types = $existing_var_type->getLiteralStrings();
+
+ if ($existing_string_types) {
+ foreach ($existing_string_types as $string_key => $literal_type) {
+ if (!$literal_type->value) {
+ $existing_var_type->removeType($string_key);
+ $did_remove_type = true;
+ }
+ }
+ } else {
+ $did_remove_type = true;
+ }
+ }
+
+ if ($existing_var_type->hasInt()) {
+ $existing_int_types = $existing_var_type->getLiteralInts();
+
+ if ($existing_int_types) {
+ foreach ($existing_int_types as $int_key => $literal_type) {
+ if (!$literal_type->value) {
+ $existing_var_type->removeType($int_key);
+ $did_remove_type = true;
+ }
+ }
+ } else {
+ $did_remove_type = true;
+ }
+ }
+
+ if ($existing_var_type->hasType('array')) {
+ $array_atomic_type = $existing_var_type->getAtomicTypes()['array'];
+
+ if ($array_atomic_type instanceof Type\Atomic\TArray
+ && !$array_atomic_type instanceof Type\Atomic\TNonEmptyArray
+ ) {
+ $did_remove_type = true;
+
+ if ($array_atomic_type->getId() === 'array') {
+ $existing_var_type->removeType('array');
+ } else {
+ $existing_var_type->addType(
+ new Type\Atomic\TNonEmptyArray(
+ $array_atomic_type->type_params
+ )
+ );
+ }
+ } elseif ($array_atomic_type instanceof Type\Atomic\TList
+ && !$array_atomic_type instanceof Type\Atomic\TNonEmptyList
+ ) {
+ $did_remove_type = true;
+
+ $existing_var_type->addType(
+ new Type\Atomic\TNonEmptyList(
+ $array_atomic_type->type_param
+ )
+ );
+ } elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike
+ && !$array_atomic_type->sealed
+ ) {
+ $did_remove_type = true;
+ }
+ }
+ }
+}