Skip to content

Commit

Permalink
handle true/false reconciliation consistently, fix #8795
Browse files Browse the repository at this point in the history
  • Loading branch information
orklah committed Nov 30, 2022
1 parent 870f581 commit 229f613
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 24 deletions.
39 changes: 15 additions & 24 deletions src/Psalm/Internal/Type/NegatedAssertionReconciler.php
Expand Up @@ -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();
Expand Down
197 changes: 197 additions & 0 deletions src/Psalm/Internal/Type/SimpleAssertionReconciler.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
102 changes: 102 additions & 0 deletions src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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']);
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 229f613

Please sign in to comment.