mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Fix if/ternary negation
This commit is contained in:
parent
9662f88631
commit
35e08f5cd2
@ -274,29 +274,41 @@ class StatementsChecker
|
||||
return false;
|
||||
}
|
||||
|
||||
$if_types = $this->_type_checker->getTypeAssertions($stmt->cond, true);
|
||||
if ($stmt->cond instanceof PhpParser\Node\Expr\BinaryOp) {
|
||||
$reconcilable_if_types = $this->_type_checker->getReconcilableTypeAssertions($stmt->cond, true);
|
||||
$negatable_if_types = $this->_type_checker->getNegatableTypeAssertions($stmt->cond, true);
|
||||
}
|
||||
else {
|
||||
$reconcilable_if_types = $negatable_if_types = $this->_type_checker->getTypeAssertions($stmt->cond, true);
|
||||
}
|
||||
|
||||
$has_leaving_statments = ScopeChecker::doesLeaveBlock($stmt->stmts, true, true);
|
||||
|
||||
// we only need to negate the if types if there are throw/return/break/continue or else/elseif blocks
|
||||
$need_to_negate_if_types = $has_leaving_statments || $stmt->elseifs || $stmt->else;
|
||||
|
||||
$can_negate_if_types = !($stmt->cond instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd);
|
||||
|
||||
$negated_types = $if_types && $need_to_negate_if_types && $can_negate_if_types
|
||||
? TypeChecker::negateTypes($if_types)
|
||||
$negated_types = $negatable_if_types && $need_to_negate_if_types
|
||||
? TypeChecker::negateTypes($negatable_if_types)
|
||||
: [];
|
||||
|
||||
$negated_if_types = $negated_types;
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if (!($stmt->cond instanceof PhpParser\Node\Expr\BinaryOp) || !self::_containsBooleanOr($stmt->cond)) {
|
||||
$if_vars_in_scope_reconciled = TypeChecker::reconcileKeyedTypes($if_types, $if_context->vars_in_scope, $this->_file_name, $stmt->getLine());
|
||||
if ($reconcilable_if_types) {
|
||||
$if_vars_in_scope_reconciled =
|
||||
TypeChecker::reconcileKeyedTypes(
|
||||
$reconcilable_if_types,
|
||||
$if_context->vars_in_scope,
|
||||
$this->_file_name,
|
||||
$stmt->getLine()
|
||||
);
|
||||
|
||||
if ($if_vars_in_scope_reconciled === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$if_context->vars_in_scope = $if_vars_in_scope_reconciled;
|
||||
$if_context->vars_possibly_in_scope = array_merge($if_types, $if_context->vars_possibly_in_scope);
|
||||
$if_context->vars_possibly_in_scope = array_merge($reconcilable_if_types, $if_context->vars_possibly_in_scope);
|
||||
}
|
||||
|
||||
$old_if_context = clone $if_context;
|
||||
@ -332,7 +344,7 @@ class StatementsChecker
|
||||
}
|
||||
|
||||
// update the parent context as necessary, but only if we can safely reason about type negation
|
||||
if ($can_negate_if_types && !$mic_drop) {
|
||||
if ($negatable_if_types && !$mic_drop) {
|
||||
$context->update($old_if_context, $if_context, $has_leaving_statments, $updated_vars);
|
||||
}
|
||||
|
||||
@ -362,19 +374,23 @@ class StatementsChecker
|
||||
$elseif_context->vars_in_scope = $elseif_vars_reconciled;
|
||||
}
|
||||
|
||||
$elseif_types = $this->_type_checker->getTypeAssertions($elseif->cond, true);
|
||||
if ($elseif->cond instanceof PhpParser\Node\Expr\BinaryOp) {
|
||||
$reconcilable_elseif_types = $this->_type_checker->getReconcilableTypeAssertions($elseif->cond, true);
|
||||
$negatable_elseif_types = $this->_type_checker->getNegatableTypeAssertions($elseif->cond, true);
|
||||
}
|
||||
else {
|
||||
$reconcilable_elseif_types = $negatable_elseif_types = $this->_type_checker->getTypeAssertions($elseif->cond, true);
|
||||
}
|
||||
|
||||
$can_negate_elseif_types = !($elseif->cond instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd);
|
||||
|
||||
$negated_elseif_types = $elseif_types && $can_negate_elseif_types
|
||||
? TypeChecker::negateTypes($elseif_types)
|
||||
$negated_elseif_types = $negatable_elseif_types
|
||||
? TypeChecker::negateTypes($negatable_elseif_types)
|
||||
: [];
|
||||
|
||||
$negated_types = array_merge($negated_types, $negated_elseif_types);
|
||||
|
||||
// if the elseif has an || in the conditional, we cannot easily reason about it
|
||||
if (!($elseif->cond instanceof PhpParser\Node\Expr\BinaryOp) || !self::_containsBooleanOr($elseif->cond)) {
|
||||
$elseif_vars_reconciled = TypeChecker::reconcileKeyedTypes($elseif_types, $elseif_context->vars_in_scope, $this->_file_name, $stmt->getLine());
|
||||
$elseif_vars_reconciled = TypeChecker::reconcileKeyedTypes($reconcilable_elseif_types, $elseif_context->vars_in_scope, $this->_file_name, $stmt->getLine());
|
||||
|
||||
if ($elseif_vars_reconciled === false) {
|
||||
return false;
|
||||
@ -443,7 +459,7 @@ class StatementsChecker
|
||||
}
|
||||
}
|
||||
|
||||
if ($can_negate_if_types) {
|
||||
if ($negatable_if_types) {
|
||||
$context->update($old_elseif_context, $elseif_context, $has_leaving_statments, $updated_vars);
|
||||
}
|
||||
|
||||
@ -531,7 +547,7 @@ class StatementsChecker
|
||||
}
|
||||
|
||||
// update the parent context as necessary
|
||||
if ($can_negate_if_types) {
|
||||
if ($negatable_if_types) {
|
||||
$context->update($old_else_context, $else_context, $has_leaving_statments, $updated_vars);
|
||||
}
|
||||
|
||||
@ -2319,13 +2335,23 @@ class StatementsChecker
|
||||
|
||||
$t_if_context = clone $context;
|
||||
|
||||
$if_types = $this->_type_checker->getTypeAssertions($stmt->cond, true);
|
||||
|
||||
$can_negate_if_types = !($stmt->cond instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd);
|
||||
if ($stmt->cond instanceof PhpParser\Node\Expr\BinaryOp) {
|
||||
$reconcilable_if_types = $this->_type_checker->getReconcilableTypeAssertions($stmt->cond, true);
|
||||
$negatable_if_types = $this->_type_checker->getNegatableTypeAssertions($stmt->cond, true);
|
||||
}
|
||||
else {
|
||||
$reconcilable_if_types = $negatable_if_types = $this->_type_checker->getTypeAssertions($stmt->cond, true);
|
||||
}
|
||||
|
||||
$if_return_type = null;
|
||||
|
||||
$t_if_vars_in_scope_reconciled = TypeChecker::reconcileKeyedTypes($if_types, $t_if_context->vars_in_scope, $this->_file_name, $stmt->getLine());
|
||||
$t_if_vars_in_scope_reconciled =
|
||||
TypeChecker::reconcileKeyedTypes(
|
||||
$reconcilable_if_types,
|
||||
$t_if_context->vars_in_scope,
|
||||
$this->_file_name,
|
||||
$stmt->getLine()
|
||||
);
|
||||
|
||||
if ($t_if_vars_in_scope_reconciled === false) {
|
||||
return false;
|
||||
@ -2341,8 +2367,9 @@ class StatementsChecker
|
||||
|
||||
$t_else_context = clone $context;
|
||||
|
||||
if ($can_negate_if_types) {
|
||||
$negated_if_types = TypeChecker::negateTypes($if_types);
|
||||
if ($negatable_if_types) {
|
||||
$negated_if_types = TypeChecker::negateTypes($negatable_if_types);
|
||||
|
||||
$t_else_vars_in_scope_reconciled = TypeChecker::reconcileKeyedTypes($negated_if_types, $t_else_context->vars_in_scope, $this->_file_name, $stmt->getLine());
|
||||
|
||||
if ($t_else_vars_in_scope_reconciled === false) {
|
||||
@ -3121,12 +3148,6 @@ class StatementsChecker
|
||||
return true;
|
||||
}
|
||||
|
||||
// or both sides are ors
|
||||
if (($stmt->left instanceof PhpParser\Node\Expr\BinaryOp && $stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) &&
|
||||
($stmt->right instanceof PhpParser\Node\Expr\BinaryOp && $stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -24,13 +24,73 @@ class TypeChecker
|
||||
$this->_checker = $statements_checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the type assertions in a conditional that are && together
|
||||
* @param PhpParser\Node\Expr $conditional [description]
|
||||
* @return [type] [description]
|
||||
*/
|
||||
public function getReconcilableTypeAssertions(PhpParser\Node\Expr $conditional)
|
||||
{
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
|
||||
$left_assertions = $this->getReconcilableTypeAssertions($conditional->left);
|
||||
$right_assertions = $this->getReconcilableTypeAssertions($conditional->right);
|
||||
|
||||
return self::combineTypeAssertions($left_assertions, $right_assertions);
|
||||
}
|
||||
|
||||
return $this->getTypeAssertions($conditional);
|
||||
}
|
||||
|
||||
public function getNegatableTypeAssertions(PhpParser\Node\Expr $conditional)
|
||||
{
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
|
||||
$left_assertions = $this->getNegatableTypeAssertions($conditional->left);
|
||||
$right_assertions = $this->getNegatableTypeAssertions($conditional->right);
|
||||
|
||||
return self::combineTypeAssertions($left_assertions, $right_assertions);
|
||||
}
|
||||
|
||||
return $this->getTypeAssertions($conditional);
|
||||
}
|
||||
|
||||
private static function combineTypeAssertions(array $left_assertions, array $right_assertions)
|
||||
{
|
||||
$keys = array_merge(array_keys($left_assertions), array_keys($right_assertions));
|
||||
$keys = array_unique($keys);
|
||||
|
||||
$if_types = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (isset($left_assertions[$key]) && isset($right_assertions[$key])) {
|
||||
$type_assertions = array_merge(explode('|', $left_assertions[$key]), explode('|', $right_assertions[$key]));
|
||||
$if_types[$key] = implode('|', array_unique($type_assertions));
|
||||
}
|
||||
else if (isset($left_assertions[$key])) {
|
||||
$if_types[$key] = $left_assertions[$key];
|
||||
}
|
||||
else {
|
||||
$if_types[$key] = $right_assertions[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $if_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the type assertions in a conditional
|
||||
*
|
||||
* @param PhpParser\Node\Expr $stmt
|
||||
* @return array
|
||||
*/
|
||||
public function getTypeAssertions(PhpParser\Node\Expr $conditional, $check_boolean_and = false)
|
||||
public function getTypeAssertions(PhpParser\Node\Expr $conditional)
|
||||
{
|
||||
$if_types = [];
|
||||
|
||||
@ -306,50 +366,6 @@ class TypeChecker
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
|
||||
$left_assertions = $this->getTypeAssertions($conditional->left, false);
|
||||
$right_assertions = $this->getTypeAssertions($conditional->right, false);
|
||||
|
||||
$keys = array_merge(array_keys($left_assertions), array_keys($right_assertions));
|
||||
$keys = array_unique($keys);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (isset($left_assertions[$key]) && isset($right_assertions[$key])) {
|
||||
$type_assertions = array_merge(explode('|', $left_assertions[$key]), explode('|', $right_assertions[$key]));
|
||||
$if_types[$key] = implode('|', array_unique($type_assertions));
|
||||
}
|
||||
else if (isset($left_assertions[$key])) {
|
||||
$if_types[$key] = $left_assertions[$key];
|
||||
}
|
||||
else {
|
||||
$if_types[$key] = $right_assertions[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ($check_boolean_and && $conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
|
||||
$left_assertions = $this->getTypeAssertions($conditional->left, $check_boolean_and);
|
||||
$right_assertions = $this->getTypeAssertions($conditional->right, $check_boolean_and);
|
||||
|
||||
$keys = array_merge(array_keys($left_assertions), array_keys($right_assertions));
|
||||
$keys = array_unique($keys);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (isset($left_assertions[$key]) && isset($right_assertions[$key])) {
|
||||
if ($left_assertions[$key][0] !== '!' && $right_assertions[$key][0] !== '!') {
|
||||
$if_types[$key] = $left_assertions[$key] . '&' . $right_assertions[$key];
|
||||
}
|
||||
else {
|
||||
$if_types[$key] = $right_assertions[$key];
|
||||
}
|
||||
}
|
||||
else if (isset($left_assertions[$key])) {
|
||||
$if_types[$key] = $left_assertions[$key];
|
||||
}
|
||||
else {
|
||||
$if_types[$key] = $right_assertions[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $if_types;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user