diff --git a/src/Psalm/Checker/CommentChecker.php b/src/Psalm/Checker/CommentChecker.php index 07792c2e0..dfed4e99d 100644 --- a/src/Psalm/Checker/CommentChecker.php +++ b/src/Psalm/Checker/CommentChecker.php @@ -636,7 +636,7 @@ class CommentChecker $last_type = null; foreach ($parsed_doc_comment['specials'] as $type => $lines) { - if ($last_type !== null && ($last_type !== 'return' || $last_type !== 'psalm-return')) { + if ($last_type !== null && $last_type !== 'psalm-return') { $doc_comment_text .= $left_padding . ' *' . "\n"; } diff --git a/src/Psalm/Checker/Statements/Block/IfChecker.php b/src/Psalm/Checker/Statements/Block/IfChecker.php index 09c9bd3a3..4b6e93298 100644 --- a/src/Psalm/Checker/Statements/Block/IfChecker.php +++ b/src/Psalm/Checker/Statements/Block/IfChecker.php @@ -159,7 +159,6 @@ class IfChecker ) ); - // get all the var ids that were referened in the conditional, but not assigned in it $cond_referenced_var_ids = array_diff_key($cond_referenced_var_ids, $cond_assigned_var_ids); @@ -1391,15 +1390,21 @@ class IfChecker /** * @param PhpParser\Node\Expr $stmt + * @param bool $inside_and * * @return PhpParser\Node\Expr|null */ - protected static function getDefinitelyEvaluatedExpression(PhpParser\Node\Expr $stmt) + protected static function getDefinitelyEvaluatedExpression(PhpParser\Node\Expr $stmt, $inside_and = false) { if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) { - if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd || - $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd || - $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor + if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd + || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd + || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor + ) { + return self::getDefinitelyEvaluatedExpression($stmt->left, true); + } elseif (!$inside_and + && ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr + || $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr) ) { return self::getDefinitelyEvaluatedExpression($stmt->left); } diff --git a/src/Psalm/Checker/Statements/Block/SwitchChecker.php b/src/Psalm/Checker/Statements/Block/SwitchChecker.php index 26d01c009..ee4f402f9 100644 --- a/src/Psalm/Checker/Statements/Block/SwitchChecker.php +++ b/src/Psalm/Checker/Statements/Block/SwitchChecker.php @@ -9,6 +9,7 @@ use Psalm\Checker\StatementsChecker; use Psalm\CodeLocation; use Psalm\Context; use Psalm\Issue\ContinueOutsideLoop; +use Psalm\Issue\ParadoxicalCondition; use Psalm\IssueBuffer; use Psalm\Scope\LoopScope; use Psalm\Type; @@ -34,6 +35,12 @@ class SwitchChecker return false; } + $switch_var_id = ExpressionChecker::getArrayVarId( + $stmt->cond, + null, + $statements_checker + ); + $original_context = clone $context; $new_vars_in_scope = null; @@ -73,6 +80,7 @@ class SwitchChecker $leftover_statements = []; $leftover_case_equality_expr = null; + $negated_clauses = []; $project_checker = $statements_checker->getFileChecker()->project_checker; @@ -102,7 +110,7 @@ class SwitchChecker return false; } - $switch_condition = $stmt->cond; + $switch_condition = clone $stmt->cond; if ($switch_condition instanceof PhpParser\Node\Expr\Variable && is_string($switch_condition->name) @@ -142,11 +150,25 @@ class SwitchChecker } } - $case_equality_expr = new PhpParser\Node\Expr\BinaryOp\Equal( - $switch_condition, - $case->cond, - $case->cond->getAttributes() - ); + if (isset($switch_condition->inferredType) + && isset($case->cond->inferredType) + && (($switch_condition->inferredType->isString() && $case->cond->inferredType->isString()) + || ($switch_condition->inferredType->isInt() && $case->cond->inferredType->isInt()) + || ($switch_condition->inferredType->isFloat() && $case->cond->inferredType->isFloat()) + ) + ) { + $case_equality_expr = new PhpParser\Node\Expr\BinaryOp\Identical( + $switch_condition, + $case->cond, + $case->cond->getAttributes() + ); + } else { + $case_equality_expr = new PhpParser\Node\Expr\BinaryOp\Equal( + $switch_condition, + $case->cond, + $case->cond->getAttributes() + ); + } } $case_stmts = $case->stmts; @@ -213,50 +235,81 @@ class SwitchChecker $leftover_statements = []; $leftover_case_equality_expr = null; + $case_clauses = []; + if ($case_equality_expr) { $case_clauses = Algebra::getFormula( $case_equality_expr, $context->self, $statements_checker ); + } + if ($negated_clauses) { + $entry_clauses = Algebra::simplifyCNF(array_merge($original_context->clauses, $negated_clauses)); + } else { + $entry_clauses = $original_context->clauses; + } + + if ($case_clauses) { // this will see whether any of the clauses in set A conflict with the clauses in set B AlgebraChecker::checkForParadox( - $context->clauses, + $entry_clauses, $case_clauses, $statements_checker, $stmt->cond, [] ); - $case_context->clauses = Algebra::simplifyCNF(array_merge($context->clauses, $case_clauses)); + $case_context->clauses = Algebra::simplifyCNF(array_merge($entry_clauses, $case_clauses)); + } else { + $case_context->clauses = $entry_clauses; + } - $reconcilable_if_types = Algebra::getTruthsFromFormula($case_context->clauses); + $reconcilable_if_types = Algebra::getTruthsFromFormula($case_context->clauses); - // if the if has an || in the conditional, we cannot easily reason about it - if ($reconcilable_if_types) { - $changed_var_ids = []; + $printer = new PhpParser\PrettyPrinter\Standard; - $case_vars_in_scope_reconciled = - Reconciler::reconcileKeyedTypes( - $reconcilable_if_types, - $case_context->vars_in_scope, - $changed_var_ids, - [], - $statements_checker, - new CodeLocation($statements_checker->getSource(), $stmt->cond, $context->include_location), - $statements_checker->getSuppressedIssues() - ); + // if the if has an || in the conditional, we cannot easily reason about it + if ($reconcilable_if_types) { + $changed_var_ids = []; - $case_context->vars_in_scope = $case_vars_in_scope_reconciled; - foreach ($reconcilable_if_types as $var_id => $_) { - $case_context->vars_possibly_in_scope[$var_id] = true; - } + $suppressed_issues = $statements_checker->getSuppressedIssues(); - if ($changed_var_ids) { - $case_context->removeReconciledClauses($changed_var_ids); - } + if (!in_array('RedundantCondition', $suppressed_issues, true)) { + $statements_checker->addSuppressedIssues(['RedundantCondition']); } + + $case_vars_in_scope_reconciled = + Reconciler::reconcileKeyedTypes( + $reconcilable_if_types, + $case_context->vars_in_scope, + $changed_var_ids, + $switch_var_id ? [$switch_var_id => true] : [], + $statements_checker, + new CodeLocation($statements_checker->getSource(), $case, $context->include_location), + $statements_checker->getSuppressedIssues() + ); + + if (!in_array('RedundantCondition', $suppressed_issues, true)) { + $statements_checker->removeSuppressedIssues(['RedundantCondition']); + } + + $case_context->vars_in_scope = $case_vars_in_scope_reconciled; + foreach ($reconcilable_if_types as $var_id => $_) { + $case_context->vars_possibly_in_scope[$var_id] = true; + } + + if ($changed_var_ids) { + $case_context->removeReconciledClauses($changed_var_ids); + } + } + + if ($case_clauses) { + $negated_clauses = array_merge( + $negated_clauses, + Algebra::negateFormula($case_clauses) + ); } $statements_checker->analyze($case_stmts, $case_context, $loop_scope); @@ -267,6 +320,21 @@ class SwitchChecker ); if ($case_exit_type !== 'return_throw') { + if (!$case->cond + && $switch_var_id + && isset($case_context->vars_in_scope[$switch_var_id]) + && $case_context->vars_in_scope[$switch_var_id]->isEmpty() + ) { + if (IssueBuffer::accepts( + new ParadoxicalCondition( + 'All possible case statements have been met, default is impossible here', + new CodeLocation($statements_checker->getSource(), $case) + ) + )) { + return false; + } + } + $vars = array_diff_key( $case_context->vars_possibly_in_scope, $original_context->vars_possibly_in_scope @@ -367,9 +435,37 @@ class SwitchChecker } } - // only update vars if there is a default - // if that default has a throw/return/continue, that should be handled above - if ($has_default) { + $all_options_matched = $has_default; + + if (!$has_default && $negated_clauses && $switch_var_id) { + $entry_clauses = Algebra::simplifyCNF(array_merge($original_context->clauses, $negated_clauses)); + + $reconcilable_if_types = Algebra::getTruthsFromFormula($entry_clauses); + + // if the if has an || in the conditional, we cannot easily reason about it + if ($reconcilable_if_types && isset($reconcilable_if_types[$switch_var_id])) { + $changed_var_ids = []; + + $case_vars_in_scope_reconciled = + Reconciler::reconcileKeyedTypes( + $reconcilable_if_types, + $original_context->vars_in_scope, + $changed_var_ids, + [], + $statements_checker + ); + + if (isset($case_vars_in_scope_reconciled[$switch_var_id]) + && $case_vars_in_scope_reconciled[$switch_var_id]->isEmpty() + ) { + $all_options_matched = true; + } + } + } + + // only update vars if there is a default or all possible cases accounted for + // if the default has a throw/return/continue, that should be handled above + if ($all_options_matched) { if ($new_vars_in_scope) { $context->vars_in_scope = array_merge($context->vars_in_scope, $new_vars_in_scope); } diff --git a/src/Psalm/Checker/Statements/Expression/AssertionFinder.php b/src/Psalm/Checker/Statements/Expression/AssertionFinder.php index 29b8b4928..0aede243c 100644 --- a/src/Psalm/Checker/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Checker/Statements/Expression/AssertionFinder.php @@ -379,7 +379,19 @@ class AssertionFinder } if ($var_name && $var_type) { - $if_types[$var_name] = '^' . $var_type->getId(); + $identical = $conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical + || ($other_type + && (($var_type->isString() && $other_type->isString()) + || ($var_type->isInt() && $other_type->isInt()) + || ($var_type->isFloat() && $other_type->isFloat()) + ) + ); + + if ($identical) { + $if_types[$var_name] = '^' . $var_type->getId(); + } else { + $if_types[$var_name] = '~' . $var_type->getId(); + } } if ($other_type @@ -697,7 +709,19 @@ class AssertionFinder if ($var_type) { if ($var_name) { - $if_types[$var_name] = '!^' . $var_type->getId(); + $not_identical = $conditional instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical + || ($other_type + && (($var_type->isString() && $other_type->isString()) + || ($var_type->isInt() && $other_type->isInt()) + || ($var_type->isFloat() && $other_type->isFloat()) + ) + ); + + if ($not_identical) { + $if_types[$var_name] = '!^' . $var_type->getId(); + } else { + $if_types[$var_name] = '!~' . $var_type->getId(); + } } if ($other_type @@ -1239,10 +1263,6 @@ class AssertionFinder */ protected static function hasTypedValueComparison(PhpParser\Node\Expr\BinaryOp $conditional) { - if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal) { - return false; - } - if (isset($conditional->right->inferredType) && count($conditional->right->inferredType->getTypes()) === 1 ) { diff --git a/src/Psalm/Checker/Statements/ExpressionChecker.php b/src/Psalm/Checker/Statements/ExpressionChecker.php index 20f0beb93..f99df9dce 100644 --- a/src/Psalm/Checker/Statements/ExpressionChecker.php +++ b/src/Psalm/Checker/Statements/ExpressionChecker.php @@ -131,7 +131,10 @@ class ExpressionChecker break; } } elseif ($stmt instanceof PhpParser\Node\Scalar\LNumber) { - $stmt->inferredType = Type::getInt(false, [$stmt->value => true]); + $stmt->inferredType = Type::getInt( + false, + [($stmt->value >= 0 ? $stmt->value : (string) $stmt->value) => true] + ); } elseif ($stmt instanceof PhpParser\Node\Scalar\DNumber) { $stmt->inferredType = Type::getFloat([(string)$stmt->value => true]); } elseif ($stmt instanceof PhpParser\Node\Expr\UnaryMinus || @@ -150,6 +153,28 @@ class ExpressionChecker foreach ($stmt->expr->inferredType->getTypes() as $type_part) { if ($type_part instanceof TInt || $type_part instanceof TFloat) { + if ($type_part instanceof Type\Atomic\TLiteralInt + && $stmt instanceof PhpParser\Node\Expr\UnaryMinus + ) { + $inverted_values = []; + + foreach ($type_part->values as $value => $_) { + $inverted_values[$value > 0 ? (string) (-$value) : (int) (-$value)] = true; + } + + $type_part->values = $inverted_values; + } elseif ($type_part instanceof Type\Atomic\TLiteralFloat + && $stmt instanceof PhpParser\Node\Expr\UnaryMinus + ) { + $inverted_values = []; + + foreach ($type_part->values as $value => $_) { + $inverted_values[(string)(-$value)] = true; + } + + $type_part->values = $inverted_values; + } + $acceptable_types[] = $type_part; } elseif ($type_part instanceof TString) { $acceptable_types[] = new TInt; diff --git a/src/Psalm/Checker/StatementsChecker.php b/src/Psalm/Checker/StatementsChecker.php index 5cda7d108..05e3e4d6d 100644 --- a/src/Psalm/Checker/StatementsChecker.php +++ b/src/Psalm/Checker/StatementsChecker.php @@ -804,7 +804,7 @@ class StatementsChecker extends SourceChecker implements StatementsSource } if ($stmt instanceof PhpParser\Node\Scalar\LNumber) { - return Type::getInt(false, [$stmt->value => true]); + return Type::getInt(false, [($stmt->value >= 0 ? $stmt->value : (string) $stmt->value) => true]); } if ($stmt instanceof PhpParser\Node\Scalar\DNumber) { @@ -929,7 +929,42 @@ class StatementsChecker extends SourceChecker implements StatementsSource } if ($stmt instanceof PhpParser\Node\Expr\UnaryMinus || $stmt instanceof PhpParser\Node\Expr\UnaryPlus) { - return self::getSimpleType($stmt->expr, $file_source, $existing_class_constants, $fq_classlike_name); + $type_to_invert = self::getSimpleType( + $stmt->expr, + $file_source, + $existing_class_constants, + $fq_classlike_name + ); + + if (!$type_to_invert) { + return null; + } + + foreach ($type_to_invert->getTypes() as $type_part) { + if ($type_part instanceof Type\Atomic\TLiteralInt + && $stmt instanceof PhpParser\Node\Expr\UnaryMinus + ) { + $inverted_values = []; + + foreach ($type_part->values as $value => $_) { + $inverted_values[$value > 0 ? (string) (-$value) : (int) (-$value)] = true; + } + + $type_part->values = $inverted_values; + } elseif ($type_part instanceof Type\Atomic\TLiteralFloat + && $stmt instanceof PhpParser\Node\Expr\UnaryMinus + ) { + $inverted_values = []; + + foreach ($type_part->values as $value => $_) { + $inverted_values[(string)(-$value)] = true; + } + + $type_part->values = $inverted_values; + } + } + + return $type_to_invert; } return null; diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 3c5931a71..09b0346e8 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -549,7 +549,7 @@ abstract class Type /** * @param bool $from_calculation - * @param array|null $values + * @param array|null $values * * @return Type\Union */ @@ -699,9 +699,6 @@ abstract class Type ] ); - /** - * @psalm-suppress InvalidScalarArgument because of a bug - */ $array_type->count = new TLiteralInt([0 => true]); return new Type\Union([ @@ -990,7 +987,6 @@ abstract class Type $array_type = new TArray($generic_type_params); if ($combination->array_counts) { - /** @psalm-suppress InvalidScalarArgument */ $array_type->count = new TLiteralInt($combination->array_counts); } @@ -1013,7 +1009,6 @@ abstract class Type } } elseif ($type instanceof TInt) { if ($combination->ints) { - /** @psalm-suppress InvalidScalarArgument */ $type = new TLiteralInt($combination->ints); } elseif ($type instanceof TLiteralInt) { $type = new TInt(); diff --git a/src/Psalm/Type/Algebra.php b/src/Psalm/Type/Algebra.php index 7bc236ae1..a6eb35d1e 100644 --- a/src/Psalm/Type/Algebra.php +++ b/src/Psalm/Type/Algebra.php @@ -544,7 +544,9 @@ class Algebra $impossibility = []; foreach ($possiblity as $type) { - if ($type[0] !== '^' && (!isset($type[1]) || $type[1] !== '^')) { + if (($type[0] !== '^' && (!isset($type[1]) || $type[1] !== '^')) + || strpos($type, '(') + ) { $impossibility[] = self::negateType($type); } } diff --git a/src/Psalm/Type/Atomic/TLiteralInt.php b/src/Psalm/Type/Atomic/TLiteralInt.php index a76af6d4a..2e9ac217d 100644 --- a/src/Psalm/Type/Atomic/TLiteralInt.php +++ b/src/Psalm/Type/Atomic/TLiteralInt.php @@ -3,11 +3,11 @@ namespace Psalm\Type\Atomic; class TLiteralInt extends TInt implements LiteralType { - /** @var array */ + /** @var array */ public $values; /** - * @param array $values + * @param array $values */ public function __construct(array $values) { @@ -23,7 +23,7 @@ class TLiteralInt extends TInt implements LiteralType } /** - * @return array + * @return array */ public function getValues() { diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 4a903040b..f9055553a 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -189,6 +189,8 @@ class Reconciler $codebase = $project_checker->codebase; $is_strict_equality = false; + $is_loose_equality = false; + $is_equality = false; $is_negation = false; if ($new_var_type[0] === '!') { @@ -199,6 +201,13 @@ class Reconciler if ($new_var_type[0] === '^') { $new_var_type = substr($new_var_type, 1); $is_strict_equality = true; + $is_equality = true; + } + + if ($new_var_type[0] === '~') { + $new_var_type = substr($new_var_type, 1); + $is_loose_equality = true; + $is_equality = true; } if ($existing_var_type === null) { @@ -234,6 +243,7 @@ class Reconciler $statements_checker, $new_var_type, $is_strict_equality, + $is_loose_equality, $existing_var_type, $old_var_type_string, $key, @@ -399,7 +409,7 @@ class Reconciler } } - if ((!$object_types || !$did_remove_type) && !$is_strict_equality) { + if ((!$object_types || !$did_remove_type) && !$is_equality) { if ($key && $code_location) { self::triggerIssueForImpossible( $existing_var_type, @@ -445,7 +455,7 @@ class Reconciler } } - if ((!$did_remove_type || !$numeric_types) && !$is_strict_equality) { + if ((!$did_remove_type || !$numeric_types) && !$is_equality) { if ($key && $code_location) { self::triggerIssueForImpossible( $existing_var_type, @@ -480,7 +490,7 @@ class Reconciler } } - if ((!$did_remove_type || !$scalar_types) && !$is_strict_equality) { + if ((!$did_remove_type || !$scalar_types) && !$is_equality) { if ($key && $code_location) { self::triggerIssueForImpossible( $existing_var_type, @@ -515,7 +525,7 @@ class Reconciler } } - if ((!$did_remove_type || !$bool_types) && !$is_strict_equality) { + if ((!$did_remove_type || !$bool_types) && !$is_equality) { if ($key && $code_location) { self::triggerIssueForImpossible( $existing_var_type, @@ -570,6 +580,7 @@ class Reconciler $new_var_type = substr($new_var_type, 9); $new_type = Type::parseString($new_var_type); $is_strict_equality = true; + $is_equality = true; } else { $bracket_pos = strpos($new_var_type, '('); @@ -642,7 +653,7 @@ class Reconciler if ($key && $new_type->getId() === $existing_var_type->getId() - && !$is_strict_equality + && !$is_equality ) { self::triggerIssueForImpossible( $existing_var_type, @@ -774,6 +785,7 @@ class Reconciler /** * @param string $new_var_type * @param bool $is_strict_equality + * @param bool $is_loose_equality * @param string $old_var_type_string * @param string|null $key * @param CodeLocation|null $code_location @@ -786,6 +798,7 @@ class Reconciler StatementsChecker $statements_checker, $new_var_type, $is_strict_equality, + $is_loose_equality, Type\Union $existing_var_type, $old_var_type_string, $key, @@ -793,8 +806,10 @@ class Reconciler $suppressed_issues, &$failed_reconciliation ) { + $is_equality = $is_strict_equality || $is_loose_equality; + // this is a specific value comparison type that cannot be negated - if ($is_strict_equality && $bracket_pos = strpos($new_var_type, '(')) { + if ($is_equality && $bracket_pos = strpos($new_var_type, '(')) { return self::handleLiteralNegatedEquality( $new_var_type, $bracket_pos, @@ -806,7 +821,7 @@ class Reconciler ); } - if (!$is_strict_equality && ($new_var_type === 'isset' || $new_var_type === 'array-key-exists')) { + if (!$is_equality && ($new_var_type === 'isset' || $new_var_type === 'array-key-exists')) { return Type::getNull(); } @@ -825,7 +840,7 @@ class Reconciler } if ((!$did_remove_type || !$non_object_types)) { - if ($key && $code_location && !$is_strict_equality) { + if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, $old_var_type_string, @@ -860,7 +875,7 @@ class Reconciler } if ((!$did_remove_type || !$non_scalar_types)) { - if ($key && $code_location && !$is_strict_equality) { + if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, $old_var_type_string, @@ -888,7 +903,7 @@ class Reconciler foreach ($existing_var_atomic_types as $type) { if (!$type instanceof TBool - || ($is_strict_equality && get_class($type) === TBool::class) + || ($is_equality && get_class($type) === TBool::class) ) { $non_bool_types[] = $type; } else { @@ -897,7 +912,7 @@ class Reconciler } if (!$did_remove_type || !$non_bool_types) { - if ($key && $code_location && !$is_strict_equality) { + if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, $old_var_type_string, @@ -932,7 +947,7 @@ class Reconciler } if ((!$non_numeric_types || !$did_remove_type)) { - if ($key && $code_location && !$is_strict_equality) { + if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, $old_var_type_string, @@ -1049,7 +1064,7 @@ class Reconciler $existing_var_type->possibly_undefined = false; if (!$did_remove_type || empty($existing_var_type->getTypes())) { - if ($key && $code_location && !$is_strict_equality) { + if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, $old_var_type_string, @@ -1080,7 +1095,7 @@ class Reconciler } if (!$did_remove_type || empty($existing_var_type->getTypes())) { - if ($key && $code_location && !$is_strict_equality) { + if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, $old_var_type_string, @@ -1142,7 +1157,7 @@ class Reconciler $existing_var_type->addType(new TNamedObject('Traversable')); } elseif (substr($new_var_type, 0, 9) === 'getclass-') { $new_var_type = substr($new_var_type, 9); - } elseif (!$is_strict_equality) { + } elseif (!$is_equality) { $existing_var_type->removeType($new_var_type); } @@ -1150,7 +1165,7 @@ class Reconciler if ($key !== '$this' || !($statements_checker->getSource()->getSource() instanceof TraitChecker) ) { - if ($key && $code_location && !$is_strict_equality) { + if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, $old_var_type_string, @@ -1343,10 +1358,18 @@ class Reconciler $ints ); - $existing_var_type->bustCache(); - $new_count = count($existing_var_atomic_types['int']->values); + if (!$existing_var_atomic_types['int']->values) { + $existing_var_type->removeType('int'); + + if (count($existing_var_atomic_types) === 1) { + $existing_var_type->addType(new TEmpty); + } + } else { + $existing_var_type->bustCache(); + } + if ($key && $code_location && count($existing_var_atomic_types) === 1 @@ -1376,10 +1399,18 @@ class Reconciler $strings ); - $existing_var_type->bustCache(); - $new_count = count($existing_var_atomic_types['string']->values); + if (!$existing_var_atomic_types['string']->values) { + $existing_var_type->removeType('string'); + + if (count($existing_var_atomic_types) === 1) { + $existing_var_type->addType(new TEmpty); + } + } else { + $existing_var_type->bustCache(); + } + if ($key && $code_location && count($existing_var_atomic_types) === 1 @@ -1409,10 +1440,18 @@ class Reconciler $floats ); - $existing_var_type->bustCache(); - $new_count = count($existing_var_atomic_types['float']->values); + if (!$existing_var_atomic_types['float']->values) { + $existing_var_type->removeType('float'); + + if (count($existing_var_atomic_types) === 1) { + $existing_var_type->addType(new TEmpty); + } + } else { + $existing_var_type->bustCache(); + } + if ($key && $code_location && count($existing_var_atomic_types) === 1 diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index b6621f5b4..68b12860e 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -699,9 +699,41 @@ class Union return $type->type_params[count($type->type_params) - 1]->isSingle(); } + /** + * @return bool true if this is an int + */ + public function isInt() + { + if (count($this->types) !== 1) { + return false; + } + return isset($this->types['float']); + } + + /** + * @return bool true if this is a float + */ + public function isFloat() + { + if (count($this->types) !== 1) { + return false; + } + return isset($this->types['float']); + } + + /** + * @return bool true if this is a string + */ + public function isString() + { + if (count($this->types) !== 1) { + return false; + } + return isset($this->types['string']); + } + /** * @return bool true if this is a string literal with only one possible value - * TODO: Is there a better place for this? */ public function isSingleStringLiteral() { diff --git a/tests/SwitchTypeTest.php b/tests/SwitchTypeTest.php index 48779f756..7cf4997ab 100644 --- a/tests/SwitchTypeTest.php +++ b/tests/SwitchTypeTest.php @@ -311,6 +311,48 @@ class SwitchTypeTest extends TestCase } }', ], + 'switchManyStrings' => [ + ' [ + ' [ + ' 'RedundantCondition - src/somefile.php:10', 'error_levels' => ['ParadoxicalCondition'], ], + 'repeatedCaseValue' => [ + ' 'ParadoxicalCondition', + ], + 'impossibleCaseValue' => [ + ' 'TypeDoesNotContainType', + ], + 'impossibleCaseDefault' => [ + ' 'ParadoxicalCondition', + ], ]; } } diff --git a/tests/TypeTest.php b/tests/TypeTest.php index 50c801825..b4074a24e 100644 --- a/tests/TypeTest.php +++ b/tests/TypeTest.php @@ -1199,7 +1199,7 @@ class TypeTest extends TestCase class B { /** @return void */ public function barBar(One $one = null) { - $a = 4; + $a = rand(0, 4); if ($one === null) { switch ($a) { @@ -1224,7 +1224,7 @@ class TypeTest extends TestCase class B { /** @return void */ public function barBar(One $one = null) { - $a = 4; + $a = rand(0, 4); if ($one === null) { switch ($a) { diff --git a/tests/ValueTest.php b/tests/ValueTest.php index 788c68904..d55027fdc 100644 --- a/tests/ValueTest.php +++ b/tests/ValueTest.php @@ -232,6 +232,50 @@ class ValueTest extends TestCase 'assertions' => [], 'error_levels' => ['MissingParamType', 'MixedMethodCall'], ], + 'regularValueReconciliation' => [ + ' [ + '