1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 12:55:26 +01:00

handle true/false reconciliation consistently, fix #8795

This commit is contained in:
orklah 2022-11-30 20:03:40 +01:00
parent 870f5817d2
commit 229f613b8e
3 changed files with 313 additions and 23 deletions

View File

@ -93,14 +93,6 @@ class NegatedAssertionReconciler extends Reconciler
$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,
@ -117,7 +109,6 @@ class NegatedAssertionReconciler extends Reconciler
if ($simple_negated_type) {
return $simple_negated_type;
}
}
$assertion_type = $assertion->getAtomicType();

View File

@ -41,6 +41,7 @@ use Psalm\Type\Atomic\TCallableString;
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 @@ class SimpleAssertionReconciler extends Reconciler
);
}
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 @@ class SimpleAssertionReconciler extends Reconciler
: 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

View File

@ -58,6 +58,7 @@ use Psalm\Type\Atomic\TResource;
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 @@ class SimpleNegatedAssertionReconciler extends Reconciler
);
}
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 @@ class SimpleNegatedAssertionReconciler extends Reconciler
$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 @@ class SimpleNegatedAssertionReconciler extends Reconciler
: 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