1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add support for checking negative values

This commit is contained in:
Matthew Brown 2018-05-12 18:46:47 -04:00
parent 759516d01f
commit 7dd86efa13
14 changed files with 461 additions and 82 deletions

View File

@ -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";
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
) {

View File

@ -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;

View File

@ -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;

View File

@ -549,7 +549,7 @@ abstract class Type
/**
* @param bool $from_calculation
* @param array<int, bool>|null $values
* @param array<string|int, bool>|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();

View File

@ -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);
}
}

View File

@ -3,11 +3,11 @@ namespace Psalm\Type\Atomic;
class TLiteralInt extends TInt implements LiteralType
{
/** @var array<int, bool> */
/** @var array<string|int, bool> */
public $values;
/**
* @param array<int, bool> $values
* @param array<string|int, bool> $values
*/
public function __construct(array $values)
{
@ -23,7 +23,7 @@ class TLiteralInt extends TInt implements LiteralType
}
/**
* @return array<int, bool>
* @return array<string|int, bool>
*/
public function getValues()
{

View File

@ -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

View File

@ -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()
{

View File

@ -311,6 +311,48 @@ class SwitchTypeTest extends TestCase
}
}',
],
'switchManyStrings' => [
'<?php
function foo(string $s) : void {
switch($s) {
case "a":
case "b":
case "c":
echo "goodbye";
}
}',
],
'allSwitchesMet' => [
'<?php
$a = rand(0, 1) ? "a" : "b";
switch ($a) {
case "a":
$foo = "hello";
break;
case "b":
$foo = "goodbye";
break;
}
echo $foo;',
],
'impossibleCaseDefaultWithThrow' => [
'<?php
$a = rand(0, 1) ? "a" : "b";
switch ($a) {
case "a":
break;
case "b":
break;
default:
throw new \Exception("should never happen");
}',
],
];
}
@ -492,6 +534,50 @@ class SwitchTypeTest extends TestCase
'error_message' => 'RedundantCondition - src/somefile.php:10',
'error_levels' => ['ParadoxicalCondition'],
],
'repeatedCaseValue' => [
'<?php
$a = rand(0, 1);
switch ($a) {
case 0:
break;
case 0:
echo "I never get here";
}',
'error_message' => 'ParadoxicalCondition',
],
'impossibleCaseValue' => [
'<?php
$a = rand(0, 1) ? "a" : "b";
switch ($a) {
case "a":
break;
case "b":
break;
case "c":
echo "impossible";
}',
'error_message' => 'TypeDoesNotContainType',
],
'impossibleCaseDefault' => [
'<?php
$a = rand(0, 1) ? "a" : "b";
switch ($a) {
case "a":
break;
case "b":
break;
default:
echo "impossible";
}',
'error_message' => 'ParadoxicalCondition',
],
];
}
}

View File

@ -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) {

View File

@ -232,6 +232,50 @@ class ValueTest extends TestCase
'assertions' => [],
'error_levels' => ['MissingParamType', 'MixedMethodCall'],
],
'regularValueReconciliation' => [
'<?php
$s = rand(0, 1) ? "a" : "b";
if (rand(0, 1)) {
$s = "c";
}
if ($s === "a" || $s === "b") {
if ($s === "a") {}
}'
],
'negativeInts' => [
'<?php
class C {
const A = 1;
const B = -1;
}
const A = 1;
const B = -1;
$i = rand(0, 1) ? A : B;
if (rand(0, 1)) {
$i = 0;
}
if ($i === A) {
echo "here";
} elseif ($i === B) {
echo "here";
}
$i = rand(0, 1) ? C::A : C::B;
if (rand(0, 1)) {
$i = 0;
}
if ($i === C::A) {
echo "here";
} elseif ($i === C::B) {
echo "here";
}',
],
];
}