mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
handle true/false reconciliation consistently, fix #8795
This commit is contained in:
parent
870f5817d2
commit
229f613b8e
@ -93,30 +93,21 @@ 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,
|
||||
$existing_var_type->freeze(),
|
||||
$key,
|
||||
$negated,
|
||||
$code_location,
|
||||
$suppressed_issues,
|
||||
$failed_reconciliation,
|
||||
$is_equality,
|
||||
$inside_loop
|
||||
);
|
||||
$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;
|
||||
}
|
||||
if ($simple_negated_type) {
|
||||
return $simple_negated_type;
|
||||
}
|
||||
|
||||
$assertion_type = $assertion->getAtomicType();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user