mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add support for checking negative values
This commit is contained in:
parent
759516d01f
commit
7dd86efa13
@ -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";
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,12 +150,26 @@ class SwitchChecker
|
||||
}
|
||||
}
|
||||
|
||||
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,41 +235,66 @@ 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);
|
||||
|
||||
$printer = new PhpParser\PrettyPrinter\Standard;
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_if_types) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
$suppressed_issues = $statements_checker->getSuppressedIssues();
|
||||
|
||||
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(), $stmt->cond, $context->include_location),
|
||||
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;
|
||||
@ -257,6 +304,12 @@ class SwitchChecker
|
||||
$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);
|
||||
}
|
||||
|
@ -379,7 +379,19 @@ class AssertionFinder
|
||||
}
|
||||
|
||||
if ($var_name && $var_type) {
|
||||
$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) {
|
||||
$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
|
||||
) {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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";
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user