2016-10-22 19:37:06 +02:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Checker\Statements\Block;
|
|
|
|
|
|
|
|
use PhpParser;
|
2017-10-23 01:53:53 +02:00
|
|
|
use Psalm\Checker\AlgebraChecker;
|
2016-10-22 19:37:06 +02:00
|
|
|
use Psalm\Checker\ScopeChecker;
|
|
|
|
use Psalm\Checker\Statements\ExpressionChecker;
|
2017-05-19 06:48:26 +02:00
|
|
|
use Psalm\Checker\StatementsChecker;
|
2016-12-04 01:11:30 +01:00
|
|
|
use Psalm\CodeLocation;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\Context;
|
2016-12-12 05:40:46 +01:00
|
|
|
use Psalm\Issue\ContinueOutsideLoop;
|
2018-05-13 00:46:47 +02:00
|
|
|
use Psalm\Issue\ParadoxicalCondition;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\IssueBuffer;
|
2017-12-03 00:28:18 +01:00
|
|
|
use Psalm\Scope\LoopScope;
|
2016-10-22 19:37:06 +02:00
|
|
|
use Psalm\Type;
|
2018-05-07 07:26:06 +02:00
|
|
|
use Psalm\Type\Algebra;
|
2017-12-29 16:55:41 +01:00
|
|
|
use Psalm\Type\Reconciler;
|
2016-10-22 19:37:06 +02:00
|
|
|
|
|
|
|
class SwitchChecker
|
|
|
|
{
|
|
|
|
/**
|
2016-11-02 07:29:00 +01:00
|
|
|
* @param StatementsChecker $statements_checker
|
|
|
|
* @param PhpParser\Node\Stmt\Switch_ $stmt
|
|
|
|
* @param Context $context
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
* @return false|null
|
2016-10-22 19:37:06 +02:00
|
|
|
*/
|
2017-01-07 21:09:47 +01:00
|
|
|
public static function analyze(
|
2016-11-02 07:29:00 +01:00
|
|
|
StatementsChecker $statements_checker,
|
|
|
|
PhpParser\Node\Stmt\Switch_ $stmt,
|
|
|
|
Context $context,
|
2017-12-03 00:28:18 +01:00
|
|
|
LoopScope $loop_scope = null
|
2016-11-02 07:29:00 +01:00
|
|
|
) {
|
2017-01-07 21:09:47 +01:00
|
|
|
if (ExpressionChecker::analyze($statements_checker, $stmt->cond, $context) === false) {
|
2016-10-22 19:37:06 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
$switch_var_id = ExpressionChecker::getArrayVarId(
|
|
|
|
$stmt->cond,
|
|
|
|
null,
|
|
|
|
$statements_checker
|
|
|
|
);
|
|
|
|
|
2016-10-22 19:37:06 +02:00
|
|
|
$original_context = clone $context;
|
|
|
|
|
|
|
|
$new_vars_in_scope = null;
|
|
|
|
|
|
|
|
$new_vars_possibly_in_scope = [];
|
|
|
|
|
|
|
|
$redefined_vars = null;
|
2017-03-14 07:00:38 +01:00
|
|
|
$possibly_redefined_vars = null;
|
2016-10-22 19:37:06 +02:00
|
|
|
|
|
|
|
// the last statement always breaks, by default
|
|
|
|
$last_case_exit_type = 'break';
|
|
|
|
|
|
|
|
$case_exit_types = new \SplFixedArray(count($stmt->cases));
|
|
|
|
|
|
|
|
$has_default = false;
|
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
$case_action_map = [];
|
|
|
|
|
2016-10-22 19:37:06 +02:00
|
|
|
// create a map of case statement -> ultimate exit type
|
2017-05-27 02:05:57 +02:00
|
|
|
for ($i = count($stmt->cases) - 1; $i >= 0; --$i) {
|
2016-10-22 19:37:06 +02:00
|
|
|
$case = $stmt->cases[$i];
|
|
|
|
|
2018-01-24 06:01:08 +01:00
|
|
|
$case_actions = $case_action_map[$i] = ScopeChecker::getFinalControlActions($case->stmts, true);
|
2017-11-28 06:25:21 +01:00
|
|
|
|
|
|
|
if (!in_array(ScopeChecker::ACTION_NONE, $case_actions, true)) {
|
|
|
|
if ($case_actions === [ScopeChecker::ACTION_END]) {
|
|
|
|
$last_case_exit_type = 'return_throw';
|
|
|
|
} elseif ($case_actions === [ScopeChecker::ACTION_CONTINUE]) {
|
|
|
|
$last_case_exit_type = 'continue';
|
|
|
|
} elseif (in_array(ScopeChecker::ACTION_BREAK, $case_actions, true)) {
|
|
|
|
$last_case_exit_type = 'break';
|
|
|
|
}
|
2016-10-22 19:37:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$case_exit_types[$i] = $last_case_exit_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
$leftover_statements = [];
|
2018-05-12 18:55:24 +02:00
|
|
|
$leftover_case_equality_expr = null;
|
2018-05-13 00:46:47 +02:00
|
|
|
$negated_clauses = [];
|
2016-10-22 19:37:06 +02:00
|
|
|
|
2018-01-21 22:24:20 +01:00
|
|
|
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
|
|
|
|
2018-05-12 18:55:24 +02:00
|
|
|
for ($i = 0, $l = count($stmt->cases); $i < $l; $i++) {
|
2016-10-22 19:37:06 +02:00
|
|
|
$case = $stmt->cases[$i];
|
2018-05-12 18:55:24 +02:00
|
|
|
|
2016-12-17 06:48:31 +01:00
|
|
|
/** @var string */
|
2016-10-22 19:37:06 +02:00
|
|
|
$case_exit_type = $case_exit_types[$i];
|
2017-09-11 17:52:34 +02:00
|
|
|
|
2018-05-12 18:55:24 +02:00
|
|
|
$case_actions = $case_action_map[$i];
|
|
|
|
|
|
|
|
// has a return/throw at end
|
|
|
|
$has_ending_statements = $case_actions === [ScopeChecker::ACTION_END];
|
|
|
|
$has_leaving_statements = $has_ending_statements
|
|
|
|
|| (count($case_actions) && !in_array(ScopeChecker::ACTION_NONE, $case_actions, true));
|
|
|
|
|
2017-09-11 17:52:34 +02:00
|
|
|
$case_context = clone $original_context;
|
2018-01-21 22:24:20 +01:00
|
|
|
if ($project_checker->alter_code) {
|
|
|
|
$case_context->branch_point = $case_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
|
|
|
}
|
2017-09-11 17:52:34 +02:00
|
|
|
$case_context->parent_context = $context;
|
2018-05-12 18:55:24 +02:00
|
|
|
|
|
|
|
$case_equality_expr = null;
|
2016-10-22 19:37:06 +02:00
|
|
|
|
|
|
|
if ($case->cond) {
|
2017-11-28 06:25:21 +01:00
|
|
|
if (ExpressionChecker::analyze($statements_checker, $case->cond, $case_context) === false) {
|
2016-10-22 19:37:06 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
$switch_condition = clone $stmt->cond;
|
2017-12-13 21:56:05 +01:00
|
|
|
|
|
|
|
if ($switch_condition instanceof PhpParser\Node\Expr\Variable
|
|
|
|
&& is_string($switch_condition->name)
|
|
|
|
&& isset($context->vars_in_scope['$' . $switch_condition->name])
|
|
|
|
) {
|
|
|
|
$switch_var_type = $context->vars_in_scope['$' . $switch_condition->name];
|
|
|
|
|
|
|
|
$type_statements = [];
|
|
|
|
|
2018-01-09 21:05:48 +01:00
|
|
|
foreach ($switch_var_type->getTypes() as $type) {
|
2017-12-13 21:56:05 +01:00
|
|
|
if ($type instanceof Type\Atomic\GetClassT) {
|
|
|
|
$type_statements[] = new PhpParser\Node\Expr\FuncCall(
|
|
|
|
new PhpParser\Node\Name(['get_class']),
|
|
|
|
[
|
|
|
|
new PhpParser\Node\Arg(
|
|
|
|
new PhpParser\Node\Expr\Variable(substr($type->typeof, 1))
|
2017-12-13 21:57:10 +01:00
|
|
|
),
|
2017-12-13 21:56:05 +01:00
|
|
|
]
|
|
|
|
);
|
|
|
|
} elseif ($type instanceof Type\Atomic\GetTypeT) {
|
|
|
|
$type_statements[] = new PhpParser\Node\Expr\FuncCall(
|
|
|
|
new PhpParser\Node\Name(['gettype']),
|
|
|
|
[
|
|
|
|
new PhpParser\Node\Arg(
|
|
|
|
new PhpParser\Node\Expr\Variable(substr($type->typeof, 1))
|
2017-12-13 21:57:10 +01:00
|
|
|
),
|
2017-12-13 21:56:05 +01:00
|
|
|
]
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$type_statements = null;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($type_statements && count($type_statements) === 1) {
|
|
|
|
$switch_condition = $type_statements[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
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()
|
|
|
|
);
|
|
|
|
}
|
2018-05-12 18:55:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$case_stmts = $case->stmts;
|
|
|
|
|
|
|
|
$case_stmts = array_merge($leftover_statements, $case_stmts);
|
|
|
|
|
|
|
|
if (!$case->cond) {
|
|
|
|
$has_default = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$has_leaving_statements && $i !== $l - 1) {
|
|
|
|
if (!$case_equality_expr) {
|
|
|
|
$case_equality_expr = new PhpParser\Node\Expr\FuncCall(
|
|
|
|
new PhpParser\Node\Name\FullyQualified(['rand']),
|
|
|
|
[
|
|
|
|
new PhpParser\Node\Arg(new PhpParser\Node\Scalar\LNumber(0)),
|
|
|
|
new PhpParser\Node\Arg(new PhpParser\Node\Scalar\LNumber(1)),
|
|
|
|
],
|
|
|
|
$case->getAttributes()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$leftover_case_equality_expr = $leftover_case_equality_expr
|
|
|
|
? new PhpParser\Node\Expr\BinaryOp\BooleanOr(
|
|
|
|
$leftover_case_equality_expr,
|
|
|
|
$case_equality_expr,
|
|
|
|
$case->cond ? $case->cond->getAttributes() : $case->getAttributes()
|
|
|
|
)
|
|
|
|
: $case_equality_expr;
|
|
|
|
|
|
|
|
$case_if_stmt = new PhpParser\Node\Stmt\If_(
|
|
|
|
$leftover_case_equality_expr,
|
|
|
|
['stmts' => $case_stmts]
|
|
|
|
);
|
|
|
|
|
|
|
|
$leftover_statements = [$case_if_stmt];
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
2017-09-11 17:52:34 +02:00
|
|
|
|
2018-05-12 18:55:24 +02:00
|
|
|
if ($leftover_case_equality_expr) {
|
|
|
|
$case_or_default_equality_expr = $case_equality_expr;
|
|
|
|
|
|
|
|
if (!$case_or_default_equality_expr) {
|
|
|
|
$case_or_default_equality_expr = new PhpParser\Node\Expr\FuncCall(
|
|
|
|
new PhpParser\Node\Name\FullyQualified(['rand']),
|
|
|
|
[
|
|
|
|
new PhpParser\Node\Arg(new PhpParser\Node\Scalar\LNumber(0)),
|
|
|
|
new PhpParser\Node\Arg(new PhpParser\Node\Scalar\LNumber(1)),
|
|
|
|
],
|
|
|
|
$case->getAttributes()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$case_equality_expr = new PhpParser\Node\Expr\BinaryOp\BooleanOr(
|
|
|
|
$leftover_case_equality_expr,
|
|
|
|
$case_or_default_equality_expr,
|
|
|
|
$case_or_default_equality_expr->getAttributes()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$case_context->inside_case = true;
|
|
|
|
|
|
|
|
$leftover_statements = [];
|
|
|
|
$leftover_case_equality_expr = null;
|
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
$case_clauses = [];
|
|
|
|
|
2018-05-12 18:55:24 +02:00
|
|
|
if ($case_equality_expr) {
|
2018-05-07 07:26:06 +02:00
|
|
|
$case_clauses = Algebra::getFormula(
|
2018-05-12 18:55:24 +02:00
|
|
|
$case_equality_expr,
|
2017-10-23 01:53:53 +02:00
|
|
|
$context->self,
|
|
|
|
$statements_checker
|
|
|
|
);
|
2018-05-13 00:46:47 +02:00
|
|
|
}
|
2017-09-11 17:52:34 +02:00
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
if ($negated_clauses) {
|
|
|
|
$entry_clauses = Algebra::simplifyCNF(array_merge($original_context->clauses, $negated_clauses));
|
|
|
|
} else {
|
|
|
|
$entry_clauses = $original_context->clauses;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($case_clauses) {
|
2017-10-23 01:53:53 +02:00
|
|
|
// this will see whether any of the clauses in set A conflict with the clauses in set B
|
2018-05-12 18:55:24 +02:00
|
|
|
AlgebraChecker::checkForParadox(
|
2018-05-13 00:46:47 +02:00
|
|
|
$entry_clauses,
|
2018-05-12 18:55:24 +02:00
|
|
|
$case_clauses,
|
|
|
|
$statements_checker,
|
2018-05-14 04:02:10 +02:00
|
|
|
$case,
|
2018-05-12 18:55:24 +02:00
|
|
|
[]
|
|
|
|
);
|
2017-10-23 01:53:53 +02:00
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
$case_context->clauses = Algebra::simplifyCNF(array_merge($entry_clauses, $case_clauses));
|
|
|
|
} else {
|
|
|
|
$case_context->clauses = $entry_clauses;
|
|
|
|
}
|
2017-10-23 01:53:53 +02:00
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
$reconcilable_if_types = Algebra::getTruthsFromFormula($case_context->clauses);
|
2017-10-23 01:53:53 +02:00
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
// if the if has an || in the conditional, we cannot easily reason about it
|
|
|
|
if ($reconcilable_if_types) {
|
|
|
|
$changed_var_ids = [];
|
2017-10-23 01:53:53 +02:00
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
$suppressed_issues = $statements_checker->getSuppressedIssues();
|
2017-12-03 06:58:24 +01:00
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
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);
|
2016-10-22 19:37:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
if ($case_clauses) {
|
|
|
|
$negated_clauses = array_merge(
|
|
|
|
$negated_clauses,
|
|
|
|
Algebra::negateFormula($case_clauses)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
$statements_checker->analyze($case_stmts, $case_context, $loop_scope);
|
2016-10-22 19:37:06 +02:00
|
|
|
|
2017-11-24 18:17:28 +01:00
|
|
|
$context->referenced_var_ids = array_merge(
|
|
|
|
$context->referenced_var_ids,
|
|
|
|
$case_context->referenced_var_ids
|
|
|
|
);
|
2017-02-01 05:24:33 +01:00
|
|
|
|
2016-10-22 19:37:06 +02:00
|
|
|
if ($case_exit_type !== 'return_throw') {
|
2018-05-13 00:46:47 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
$vars = array_diff_key(
|
|
|
|
$case_context->vars_possibly_in_scope,
|
|
|
|
$original_context->vars_possibly_in_scope
|
|
|
|
);
|
2016-10-22 19:37:06 +02:00
|
|
|
|
|
|
|
// if we're leaving this block, add vars to outer for loop scope
|
|
|
|
if ($case_exit_type === 'continue') {
|
2017-12-03 00:28:18 +01:00
|
|
|
if ($loop_scope) {
|
|
|
|
$loop_scope->vars_possibly_in_scope = array_merge(
|
2016-11-02 07:29:00 +01:00
|
|
|
$vars,
|
2017-12-03 00:28:18 +01:00
|
|
|
$loop_scope->vars_possibly_in_scope
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
|
|
|
} else {
|
2016-10-22 19:37:06 +02:00
|
|
|
if (IssueBuffer::accepts(
|
2016-12-12 05:40:46 +01:00
|
|
|
new ContinueOutsideLoop(
|
2016-10-22 19:37:06 +02:00
|
|
|
'Continue called when not in loop',
|
2016-12-04 01:11:30 +01:00
|
|
|
new CodeLocation($statements_checker->getSource(), $case)
|
2016-10-22 19:37:06 +02:00
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2017-12-03 00:28:18 +01:00
|
|
|
$case_redefined_vars = $case_context->getRedefinedVars($original_context->vars_in_scope);
|
2016-10-22 19:37:06 +02:00
|
|
|
|
2017-03-14 07:00:38 +01:00
|
|
|
if ($possibly_redefined_vars === null) {
|
|
|
|
$possibly_redefined_vars = $case_redefined_vars;
|
|
|
|
} else {
|
|
|
|
foreach ($case_redefined_vars as $var_id => $type) {
|
|
|
|
if (!isset($possibly_redefined_vars[$var_id])) {
|
|
|
|
$possibly_redefined_vars[$var_id] = $type;
|
|
|
|
} else {
|
|
|
|
$possibly_redefined_vars[$var_id] = Type::combineUnionTypes(
|
|
|
|
$type,
|
|
|
|
$possibly_redefined_vars[$var_id]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-22 19:37:06 +02:00
|
|
|
if ($redefined_vars === null) {
|
|
|
|
$redefined_vars = $case_redefined_vars;
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2017-12-03 00:06:21 +01:00
|
|
|
foreach ($redefined_vars as $var_id => $type) {
|
|
|
|
if (!isset($case_redefined_vars[$var_id])) {
|
|
|
|
unset($redefined_vars[$var_id]);
|
|
|
|
} else {
|
|
|
|
$redefined_vars[$var_id] = Type::combineUnionTypes(
|
|
|
|
$type,
|
|
|
|
$case_redefined_vars[$var_id]
|
|
|
|
);
|
2016-10-22 19:37:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-27 19:58:58 +01:00
|
|
|
$context_new_vars = array_diff_key($case_context->vars_in_scope, $context->vars_in_scope);
|
|
|
|
|
2016-10-22 19:37:06 +02:00
|
|
|
if ($new_vars_in_scope === null) {
|
2016-12-27 19:58:58 +01:00
|
|
|
$new_vars_in_scope = $context_new_vars;
|
2016-11-02 07:29:00 +01:00
|
|
|
$new_vars_possibly_in_scope = array_diff_key(
|
|
|
|
$case_context->vars_possibly_in_scope,
|
|
|
|
$context->vars_possibly_in_scope
|
|
|
|
);
|
|
|
|
} else {
|
2016-12-28 20:38:16 +01:00
|
|
|
foreach ($new_vars_in_scope as $new_var => $type) {
|
2018-01-28 23:28:34 +01:00
|
|
|
if (!$case_context->hasVariable($new_var, $statements_checker)) {
|
2016-10-22 19:37:06 +02:00
|
|
|
unset($new_vars_in_scope[$new_var]);
|
2016-12-27 19:58:58 +01:00
|
|
|
} else {
|
2016-12-28 20:38:16 +01:00
|
|
|
$new_vars_in_scope[$new_var] =
|
|
|
|
Type::combineUnionTypes($case_context->vars_in_scope[$new_var], $type);
|
2016-10-22 19:37:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$new_vars_possibly_in_scope = array_merge(
|
|
|
|
array_diff_key(
|
|
|
|
$case_context->vars_possibly_in_scope,
|
|
|
|
$context->vars_possibly_in_scope
|
|
|
|
),
|
|
|
|
$new_vars_possibly_in_scope
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
if ($context->collect_references) {
|
|
|
|
foreach ($case_context->unreferenced_vars as $var_id => $location) {
|
|
|
|
if (isset($context->unreferenced_vars[$var_id])
|
|
|
|
&& $context->unreferenced_vars[$var_id] !== $location
|
|
|
|
) {
|
|
|
|
$context->hasVariable($var_id, $statements_checker);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$context->unreferenced_vars = array_merge(
|
|
|
|
$context->unreferenced_vars,
|
|
|
|
$case_context->unreferenced_vars
|
|
|
|
);
|
|
|
|
}
|
2016-10-22 19:37:06 +02:00
|
|
|
}
|
|
|
|
|
2018-05-13 00:46:47 +02:00
|
|
|
$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) {
|
2016-10-22 19:37:06 +02:00
|
|
|
if ($new_vars_in_scope) {
|
|
|
|
$context->vars_in_scope = array_merge($context->vars_in_scope, $new_vars_in_scope);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($redefined_vars) {
|
|
|
|
$context->vars_in_scope = array_merge($context->vars_in_scope, $redefined_vars);
|
|
|
|
}
|
2017-11-21 02:39:27 +01:00
|
|
|
|
|
|
|
if ($possibly_redefined_vars) {
|
|
|
|
foreach ($possibly_redefined_vars as $var_id => $type) {
|
|
|
|
if (!isset($redefined_vars[$var_id]) && !isset($new_vars_in_scope[$var_id])) {
|
|
|
|
$context->vars_in_scope[$var_id]
|
|
|
|
= Type::combineUnionTypes($type, $context->vars_in_scope[$var_id]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-03-14 07:00:38 +01:00
|
|
|
} elseif ($possibly_redefined_vars) {
|
|
|
|
foreach ($possibly_redefined_vars as $var_id => $type) {
|
2017-03-14 02:15:46 +01:00
|
|
|
$context->vars_in_scope[$var_id] = Type::combineUnionTypes($type, $context->vars_in_scope[$var_id]);
|
|
|
|
}
|
2016-10-22 19:37:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$context->vars_possibly_in_scope = array_merge($context->vars_possibly_in_scope, $new_vars_possibly_in_scope);
|
2017-05-25 04:07:49 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-10-22 19:37:06 +02:00
|
|
|
}
|
|
|
|
}
|