2016-10-22 19:23:18 +02:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Checker\Statements\Block;
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
use PhpParser;
|
2016-10-22 19:23:18 +02:00
|
|
|
use Psalm\Checker\ScopeChecker;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\Checker\Statements\ExpressionChecker;
|
2016-10-22 19:23:18 +02:00
|
|
|
use Psalm\Checker\StatementsChecker;
|
|
|
|
use Psalm\Checker\TypeChecker;
|
2016-12-27 19:58:58 +01:00
|
|
|
use Psalm\Clause;
|
2016-12-04 01:11:30 +01:00
|
|
|
use Psalm\CodeLocation;
|
2016-10-22 19:23:18 +02:00
|
|
|
use Psalm\Context;
|
2016-11-11 23:13:13 +01:00
|
|
|
use Psalm\IfScope;
|
2017-02-23 06:25:28 +01:00
|
|
|
use Psalm\IssueBuffer;
|
|
|
|
use Psalm\Issue\ConflictingReferenceConstraint;
|
2016-10-22 19:23:18 +02:00
|
|
|
use Psalm\Type;
|
|
|
|
|
|
|
|
class IfChecker
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* System of type substitution and deletion
|
|
|
|
*
|
|
|
|
* for example
|
|
|
|
*
|
|
|
|
* x: A|null
|
|
|
|
*
|
|
|
|
* if (x)
|
|
|
|
* (x: A)
|
|
|
|
* x = B -- effects: remove A from the type of x, add B
|
|
|
|
* else
|
|
|
|
* (x: null)
|
|
|
|
* x = C -- effects: remove null from the type of x, add C
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* x: A|null
|
|
|
|
*
|
|
|
|
* if (!x)
|
|
|
|
* (x: null)
|
|
|
|
* throw new Exception -- effects: remove null from the type of x
|
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
* @param StatementsChecker $statements_checker
|
2016-10-22 19:23:18 +02:00
|
|
|
* @param PhpParser\Node\Stmt\If_ $stmt
|
|
|
|
* @param Context $context
|
|
|
|
* @param Context|null $loop_context
|
|
|
|
* @return null|false
|
|
|
|
*/
|
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\If_ $stmt,
|
|
|
|
Context $context,
|
|
|
|
Context $loop_context = null
|
|
|
|
) {
|
2016-10-22 19:23:18 +02:00
|
|
|
// get the first expression in the if, which should be evaluated on its own
|
|
|
|
// this allows us to update the context of $matches in
|
|
|
|
// if (!preg_match('/a/', 'aa', $matches)) {
|
|
|
|
// exit
|
|
|
|
// }
|
|
|
|
// echo $matches[0];
|
2016-12-28 23:04:03 +01:00
|
|
|
$first_if_cond_expr = self::getDefinitelyEvaluatedExpression($stmt->cond);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2017-02-13 00:06:18 +01:00
|
|
|
$context->inside_conditional = true;
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
if ($first_if_cond_expr &&
|
2017-01-07 21:09:47 +01:00
|
|
|
ExpressionChecker::analyze($statements_checker, $first_if_cond_expr, $context) === false
|
2016-11-02 07:29:00 +01:00
|
|
|
) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-02-13 00:06:18 +01:00
|
|
|
$context->inside_conditional = false;
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope = new IfScope();
|
|
|
|
|
|
|
|
$if_scope->loop_context = $loop_context;
|
2017-03-16 19:45:45 +01:00
|
|
|
$if_scope->has_elseifs = count($stmt->elseifs) > 0;
|
2016-11-11 23:13:13 +01:00
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
$if_context = clone $context;
|
|
|
|
|
|
|
|
// we need to clone the current context so our ongoing updates to $context don't mess with elseif/else blocks
|
|
|
|
$original_context = clone $context;
|
|
|
|
|
2017-02-13 00:06:18 +01:00
|
|
|
$if_context->inside_conditional = true;
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
if ($first_if_cond_expr !== $stmt->cond &&
|
2017-01-07 21:09:47 +01:00
|
|
|
ExpressionChecker::analyze($statements_checker, $stmt->cond, $if_context) === false
|
2016-11-02 07:29:00 +01:00
|
|
|
) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-02-13 00:06:18 +01:00
|
|
|
$if_context->inside_conditional = false;
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
$reconcilable_if_types = null;
|
|
|
|
|
2016-12-27 19:58:58 +01:00
|
|
|
$if_clauses = TypeChecker::getFormula(
|
|
|
|
$stmt->cond,
|
2017-01-07 20:35:07 +01:00
|
|
|
$context->self,
|
|
|
|
$statements_checker
|
2016-12-27 19:58:58 +01:00
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-12-27 19:58:58 +01:00
|
|
|
$if_context->clauses = TypeChecker::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2017-03-16 19:45:45 +01:00
|
|
|
$if_scope->negated_clauses = TypeChecker::negateFormula($if_clauses);
|
2016-12-27 19:58:58 +01:00
|
|
|
|
2017-03-16 19:45:45 +01:00
|
|
|
$if_scope->negated_types = TypeChecker::getTruthsFromFormula($if_scope->negated_clauses);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-12-27 19:58:58 +01:00
|
|
|
$reconcilable_if_types = TypeChecker::getTruthsFromFormula($if_context->clauses);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
|
|
// if the if has an || in the conditional, we cannot easily reason about it
|
|
|
|
if ($reconcilable_if_types) {
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars = [];
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
$if_vars_in_scope_reconciled =
|
|
|
|
TypeChecker::reconcileKeyedTypes(
|
|
|
|
$reconcilable_if_types,
|
|
|
|
$if_context->vars_in_scope,
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars,
|
2017-01-02 21:31:18 +01:00
|
|
|
$statements_checker->getFileChecker(),
|
2016-12-11 19:59:36 +01:00
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->cond),
|
2016-10-22 19:23:18 +02:00
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($if_vars_in_scope_reconciled === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-12-28 20:20:16 +01:00
|
|
|
foreach ($changed_vars as $changed_var) {
|
|
|
|
$if_context->removeVarFromClauses($changed_var);
|
|
|
|
}
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
$if_context->vars_in_scope = $if_vars_in_scope_reconciled;
|
2016-11-02 07:29:00 +01:00
|
|
|
$if_context->vars_possibly_in_scope = array_merge(
|
|
|
|
$reconcilable_if_types,
|
|
|
|
$if_context->vars_possibly_in_scope
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$old_if_context = clone $if_context;
|
2016-11-02 07:29:00 +01:00
|
|
|
$context->vars_possibly_in_scope = array_merge(
|
|
|
|
$if_context->vars_possibly_in_scope,
|
|
|
|
$context->vars_possibly_in_scope
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2017-02-27 05:09:18 +01:00
|
|
|
if ($context->collect_references) {
|
2017-02-01 05:24:33 +01:00
|
|
|
$context->referenced_vars = array_merge(
|
|
|
|
$if_context->referenced_vars,
|
|
|
|
$context->referenced_vars
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$temp_else_context = clone $original_context;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($if_scope->negated_types) {
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars = [];
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
$else_vars_reconciled = TypeChecker::reconcileKeyedTypes(
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->negated_types,
|
|
|
|
$temp_else_context->vars_in_scope,
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars,
|
2017-01-02 21:31:18 +01:00
|
|
|
$statements_checker->getFileChecker(),
|
2016-12-11 19:59:36 +01:00
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->cond),
|
2016-10-22 19:23:18 +02:00
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($else_vars_reconciled === false) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$temp_else_context->vars_in_scope = $else_vars_reconciled;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// we calculate the vars redefined in a hypothetical else statement to determine
|
|
|
|
// which vars of the if we can safely change
|
2016-11-11 23:13:13 +01:00
|
|
|
$pre_assignment_else_redefined_vars = Context::getRedefinedVars($context, $temp_else_context);
|
|
|
|
|
|
|
|
// check the if
|
2017-01-07 21:09:47 +01:00
|
|
|
self::analyzeIfBlock(
|
2016-11-11 23:13:13 +01:00
|
|
|
$statements_checker,
|
|
|
|
$stmt,
|
|
|
|
$if_scope,
|
|
|
|
$if_context,
|
|
|
|
$old_if_context,
|
|
|
|
$context,
|
|
|
|
$pre_assignment_else_redefined_vars
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
// check the elseifs
|
|
|
|
foreach ($stmt->elseifs as $elseif) {
|
2016-12-27 19:58:58 +01:00
|
|
|
$elseif_context = clone $original_context;
|
|
|
|
|
2017-01-07 21:09:47 +01:00
|
|
|
self::analyzeElseIfBlock(
|
2016-11-11 23:13:13 +01:00
|
|
|
$statements_checker,
|
|
|
|
$elseif,
|
|
|
|
$if_scope,
|
2016-12-27 19:58:58 +01:00
|
|
|
$elseif_context,
|
2017-03-16 19:45:45 +01:00
|
|
|
$context
|
2016-11-11 23:13:13 +01:00
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
// check the else
|
|
|
|
if ($stmt->else) {
|
2016-12-27 19:58:58 +01:00
|
|
|
$else_context = clone $original_context;
|
|
|
|
|
2017-01-07 21:09:47 +01:00
|
|
|
self::analyzeElseBlock(
|
2016-11-11 23:13:13 +01:00
|
|
|
$statements_checker,
|
|
|
|
$stmt->else,
|
|
|
|
$if_scope,
|
2016-12-27 19:58:58 +01:00
|
|
|
$else_context,
|
2017-03-16 19:45:45 +01:00
|
|
|
$context
|
2016-11-11 23:13:13 +01:00
|
|
|
);
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2017-02-01 05:24:33 +01:00
|
|
|
$context->vars_possibly_in_scope = array_merge(
|
|
|
|
$context->vars_possibly_in_scope,
|
|
|
|
$if_scope->new_vars_possibly_in_scope
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
|
|
$updated_loop_vars = [];
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
// vars can only be defined/redefined if there was an else (defined in every block)
|
|
|
|
if ($stmt->else) {
|
|
|
|
if ($if_scope->new_vars) {
|
|
|
|
$context->vars_in_scope = array_merge($context->vars_in_scope, $if_scope->new_vars);
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($if_scope->redefined_vars) {
|
|
|
|
foreach ($if_scope->redefined_vars as $var => $type) {
|
|
|
|
$context->vars_in_scope[$var] = $type;
|
|
|
|
$if_scope->updated_vars[$var] = true;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($if_scope->redefined_loop_vars && $loop_context) {
|
|
|
|
foreach ($if_scope->redefined_loop_vars as $var => $type) {
|
|
|
|
$loop_context->vars_in_scope[$var] = $type;
|
|
|
|
$updated_loop_vars[$var] = true;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
} else {
|
|
|
|
if ($if_scope->forced_new_vars) {
|
|
|
|
$context->vars_in_scope = array_merge($context->vars_in_scope, $if_scope->forced_new_vars);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($if_scope->possibly_redefined_vars) {
|
|
|
|
foreach ($if_scope->possibly_redefined_vars as $var => $type) {
|
2017-03-13 23:06:56 +01:00
|
|
|
if (!$type->failed_reconciliation &&
|
|
|
|
$context->hasVariable($var) &&
|
|
|
|
!isset($if_scope->updated_vars[$var])
|
|
|
|
) {
|
2016-11-11 23:13:13 +01:00
|
|
|
$context->vars_in_scope[$var] = Type::combineUnionTypes($context->vars_in_scope[$var], $type);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($if_scope->possibly_redefined_loop_vars && $loop_context) {
|
|
|
|
foreach ($if_scope->possibly_redefined_loop_vars as $var => $type) {
|
2017-02-01 05:24:33 +01:00
|
|
|
if ($loop_context->hasVariable($var) && !isset($updated_loop_vars[$var])) {
|
2016-11-11 23:13:13 +01:00
|
|
|
$loop_context->vars_in_scope[$var] = Type::combineUnionTypes(
|
|
|
|
$loop_context->vars_in_scope[$var],
|
|
|
|
$type
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
return null;
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
/**
|
|
|
|
* @param StatementsChecker $statements_checker
|
|
|
|
* @param PhpParser\Node\Stmt\If_ $stmt
|
|
|
|
* @param IfScope $if_scope
|
|
|
|
* @param Context $if_context
|
|
|
|
* @param Context $old_if_context
|
|
|
|
* @param Context $outer_context
|
|
|
|
* @param array<string,Type\Union> $pre_assignment_else_redefined_vars
|
|
|
|
* @return false|null
|
|
|
|
*/
|
2017-01-07 21:09:47 +01:00
|
|
|
protected static function analyzeIfBlock(
|
2016-11-11 23:13:13 +01:00
|
|
|
StatementsChecker $statements_checker,
|
|
|
|
PhpParser\Node\Stmt\If_ $stmt,
|
|
|
|
IfScope $if_scope,
|
|
|
|
Context $if_context,
|
|
|
|
Context $old_if_context,
|
|
|
|
Context $outer_context,
|
|
|
|
array $pre_assignment_else_redefined_vars
|
|
|
|
) {
|
|
|
|
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($stmt->stmts);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$has_leaving_statements = $has_ending_statements || ScopeChecker::doesAlwaysBreakOrContinue($stmt->stmts);
|
|
|
|
|
2017-01-07 21:09:47 +01:00
|
|
|
if ($statements_checker->analyze($stmt->stmts, $if_context, $if_scope->loop_context) === false) {
|
2016-11-11 23:13:13 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-03-16 19:45:45 +01:00
|
|
|
$if_scope->reasonable_clauses = $if_context->clauses;
|
|
|
|
|
2017-02-23 06:25:28 +01:00
|
|
|
if ($if_context->byref_constraints !== null) {
|
|
|
|
foreach ($if_context->byref_constraints as $var_id => $byref_constraint) {
|
|
|
|
if ($outer_context->byref_constraints !== null &&
|
|
|
|
isset($outer_context->byref_constraints[$var_id]) &&
|
|
|
|
!TypeChecker::isContainedBy(
|
|
|
|
$byref_constraint->type,
|
|
|
|
$outer_context->byref_constraints[$var_id]->type,
|
|
|
|
$statements_checker->getFileChecker()
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new ConflictingReferenceConstraint(
|
|
|
|
'There is more than one pass-by--reference constraint on ' . $var_id,
|
|
|
|
new CodeLocation($statements_checker, $stmt, true)
|
|
|
|
),
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$outer_context->byref_constraints[$var_id] = $byref_constraint;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-27 05:09:18 +01:00
|
|
|
if ($outer_context->collect_references) {
|
2017-02-01 05:24:33 +01:00
|
|
|
$outer_context->referenced_vars = array_merge(
|
|
|
|
$outer_context->referenced_vars,
|
|
|
|
$if_context->referenced_vars
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$mic_drop = false;
|
2016-11-02 07:29:00 +01:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if (!$has_leaving_statements) {
|
|
|
|
$if_scope->new_vars = array_diff_key($if_context->vars_in_scope, $outer_context->vars_in_scope);
|
|
|
|
|
|
|
|
// if we have a check like if (!isset($a)) { $a = true; } we want to make sure $a is always set
|
2017-02-11 01:10:13 +01:00
|
|
|
foreach ($if_scope->new_vars as $var_id => $_) {
|
2017-01-31 06:34:06 +01:00
|
|
|
if (isset($if_scope->negated_types[$var_id]) && $if_scope->negated_types[$var_id] === 'isset') {
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->forced_new_vars[$var_id] = Type::getMixed();
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->redefined_vars = Context::getRedefinedVars($outer_context, $if_context);
|
|
|
|
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
|
2017-03-16 16:46:07 +01:00
|
|
|
} elseif (!$stmt->else && !$stmt->elseifs) {
|
|
|
|
if ($if_scope->negated_types) {
|
|
|
|
$changed_vars = [];
|
2016-12-28 20:20:16 +01:00
|
|
|
|
2017-03-16 16:46:07 +01:00
|
|
|
$outer_context_vars_reconciled = TypeChecker::reconcileKeyedTypes(
|
|
|
|
$if_scope->negated_types,
|
|
|
|
$outer_context->vars_in_scope,
|
|
|
|
$changed_vars,
|
|
|
|
$statements_checker->getFileChecker(),
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->cond),
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
);
|
2016-11-02 07:29:00 +01:00
|
|
|
|
2017-03-16 16:46:07 +01:00
|
|
|
foreach ($changed_vars as $changed_var) {
|
|
|
|
$outer_context->removeVarFromClauses($changed_var);
|
|
|
|
}
|
2016-12-29 00:55:16 +01:00
|
|
|
|
2017-03-16 16:46:07 +01:00
|
|
|
if ($outer_context_vars_reconciled === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$outer_context->vars_in_scope = $outer_context_vars_reconciled;
|
|
|
|
$mic_drop = true;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
2017-03-16 16:46:07 +01:00
|
|
|
$outer_context->clauses = TypeChecker::simplifyCNF(
|
2017-03-16 19:45:45 +01:00
|
|
|
array_merge($outer_context->clauses, $if_scope->negated_clauses)
|
2017-03-16 16:46:07 +01:00
|
|
|
);
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
// update the parent context as necessary, but only if we can safely reason about type negation.
|
|
|
|
// We only update vars that changed both at the start of the if block and then again by an assignment
|
|
|
|
// in the if statement.
|
2016-12-27 19:58:58 +01:00
|
|
|
if ($if_scope->negated_types && !$mic_drop) {
|
2016-12-29 00:55:16 +01:00
|
|
|
$vars_to_update = array_intersect(
|
|
|
|
array_keys($pre_assignment_else_redefined_vars),
|
|
|
|
array_keys($if_scope->negated_types)
|
|
|
|
);
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$outer_context->update(
|
|
|
|
$old_if_context,
|
|
|
|
$if_context,
|
|
|
|
$has_leaving_statements,
|
2016-12-29 00:55:16 +01:00
|
|
|
$vars_to_update,
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->updated_vars
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if (!$has_ending_statements) {
|
|
|
|
$vars = array_diff_key($if_context->vars_possibly_in_scope, $outer_context->vars_possibly_in_scope);
|
|
|
|
|
|
|
|
if ($has_leaving_statements && $if_scope->loop_context) {
|
|
|
|
$if_scope->redefined_loop_vars = Context::getRedefinedVars($if_scope->loop_context, $if_context);
|
|
|
|
$if_scope->possibly_redefined_loop_vars = $if_scope->redefined_loop_vars;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we're leaving this block, add vars to outer for loop scope
|
|
|
|
if ($has_leaving_statements) {
|
|
|
|
if ($if_scope->loop_context) {
|
|
|
|
$if_scope->loop_context->vars_possibly_in_scope = array_merge(
|
|
|
|
$if_scope->loop_context->vars_possibly_in_scope,
|
|
|
|
$vars
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
} else {
|
|
|
|
$if_scope->new_vars_possibly_in_scope = $vars;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
/**
|
|
|
|
* @param StatementsChecker $statements_checker
|
|
|
|
* @param PhpParser\Node\Stmt\ElseIf_ $elseif
|
|
|
|
* @param IfScope $if_scope
|
|
|
|
* @param Context $elseif_context
|
|
|
|
* @param Context $outer_context
|
|
|
|
* @return false|null
|
|
|
|
*/
|
2017-01-07 21:09:47 +01:00
|
|
|
protected static function analyzeElseIfBlock(
|
2016-11-11 23:13:13 +01:00
|
|
|
StatementsChecker $statements_checker,
|
|
|
|
PhpParser\Node\Stmt\ElseIf_ $elseif,
|
|
|
|
IfScope $if_scope,
|
|
|
|
Context $elseif_context,
|
2017-03-16 19:45:45 +01:00
|
|
|
Context $outer_context
|
2016-11-11 23:13:13 +01:00
|
|
|
) {
|
|
|
|
$original_context = clone $elseif_context;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($if_scope->negated_types) {
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars = [];
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$elseif_vars_reconciled = TypeChecker::reconcileKeyedTypes(
|
|
|
|
$if_scope->negated_types,
|
|
|
|
$elseif_context->vars_in_scope,
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars,
|
2017-01-02 21:31:18 +01:00
|
|
|
$statements_checker->getFileChecker(),
|
2016-12-11 19:59:36 +01:00
|
|
|
new CodeLocation($statements_checker->getSource(), $elseif->cond),
|
2016-11-11 23:13:13 +01:00
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($elseif_vars_reconciled === false) {
|
|
|
|
return false;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$elseif_context->vars_in_scope = $elseif_vars_reconciled;
|
|
|
|
}
|
|
|
|
|
2017-03-14 20:48:52 +01:00
|
|
|
$pre_conditional_context = clone $elseif_context;
|
|
|
|
|
2017-02-13 00:06:18 +01:00
|
|
|
$elseif_context->inside_conditional = true;
|
|
|
|
|
2016-12-09 18:06:14 +01:00
|
|
|
// check the elseif
|
2017-01-07 21:09:47 +01:00
|
|
|
if (ExpressionChecker::analyze($statements_checker, $elseif->cond, $elseif_context) === false) {
|
2016-12-09 18:06:14 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-02-13 00:06:18 +01:00
|
|
|
$elseif_context->inside_conditional = false;
|
|
|
|
|
2016-12-27 19:58:58 +01:00
|
|
|
$elseif_clauses = TypeChecker::getFormula(
|
|
|
|
$elseif->cond,
|
|
|
|
$statements_checker->getFQCLN(),
|
2017-01-07 20:35:07 +01:00
|
|
|
$statements_checker
|
2016-12-27 19:58:58 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
$elseif_context->clauses = TypeChecker::simplifyCNF(
|
|
|
|
array_merge(
|
|
|
|
$original_context->clauses,
|
2017-03-16 19:45:45 +01:00
|
|
|
$if_scope->negated_clauses,
|
2016-12-27 19:58:58 +01:00
|
|
|
$elseif_clauses
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
$reconcilable_elseif_types = TypeChecker::getTruthsFromFormula($elseif_context->clauses);
|
|
|
|
$negated_elseif_types = TypeChecker::getTruthsFromFormula(TypeChecker::negateFormula($elseif_clauses));
|
2016-11-11 23:13:13 +01:00
|
|
|
|
|
|
|
$all_negated_vars = array_unique(
|
|
|
|
array_merge(
|
|
|
|
array_keys($negated_elseif_types),
|
|
|
|
array_keys($if_scope->negated_types)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
foreach ($all_negated_vars as $var_id) {
|
|
|
|
if (isset($negated_elseif_types[$var_id])) {
|
|
|
|
if (isset($if_scope->negated_types[$var_id])) {
|
|
|
|
$if_scope->negated_types[$var_id] = $if_scope->negated_types[$var_id] . '&' . $negated_elseif_types[$var_id];
|
|
|
|
} else {
|
|
|
|
$if_scope->negated_types[$var_id] = $negated_elseif_types[$var_id];
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
// if the elseif has an || in the conditional, we cannot easily reason about it
|
|
|
|
if ($reconcilable_elseif_types) {
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars = [];
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$elseif_vars_reconciled = TypeChecker::reconcileKeyedTypes(
|
|
|
|
$reconcilable_elseif_types,
|
|
|
|
$elseif_context->vars_in_scope,
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars,
|
2017-01-02 21:31:18 +01:00
|
|
|
$statements_checker->getFileChecker(),
|
2016-12-11 19:59:36 +01:00
|
|
|
new CodeLocation($statements_checker->getSource(), $elseif->cond),
|
2016-11-11 23:13:13 +01:00
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($elseif_vars_reconciled === false) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$elseif_context->vars_in_scope = $elseif_vars_reconciled;
|
|
|
|
}
|
|
|
|
|
|
|
|
$old_elseif_context = clone $elseif_context;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2017-01-07 21:09:47 +01:00
|
|
|
if ($statements_checker->analyze($elseif->stmts, $elseif_context, $if_scope->loop_context) === false) {
|
2016-11-11 23:13:13 +01:00
|
|
|
return false;
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2017-02-23 06:25:28 +01:00
|
|
|
if ($elseif_context->byref_constraints !== null) {
|
|
|
|
foreach ($elseif_context->byref_constraints as $var_id => $byref_constraint) {
|
|
|
|
if ($outer_context->byref_constraints !== null &&
|
|
|
|
isset($outer_context->byref_constraints[$var_id]) &&
|
|
|
|
!TypeChecker::isContainedBy(
|
|
|
|
$byref_constraint->type,
|
|
|
|
$outer_context->byref_constraints[$var_id]->type,
|
|
|
|
$statements_checker->getFileChecker()
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new ConflictingReferenceConstraint(
|
|
|
|
'There is more than one pass-by--reference constraint on ' . $var_id,
|
|
|
|
new CodeLocation($statements_checker, $elseif, true)
|
|
|
|
),
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$outer_context->byref_constraints[$var_id] = $byref_constraint;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if (count($elseif->stmts)) {
|
|
|
|
// has a return/throw at end
|
|
|
|
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($elseif->stmts);
|
|
|
|
|
|
|
|
$has_leaving_statements = $has_ending_statements ||
|
|
|
|
ScopeChecker::doesAlwaysBreakOrContinue($elseif->stmts);
|
|
|
|
|
|
|
|
// update the parent context as necessary
|
|
|
|
$elseif_redefined_vars = Context::getRedefinedVars($original_context, $elseif_context);
|
|
|
|
|
|
|
|
if (!$has_leaving_statements) {
|
|
|
|
if ($if_scope->new_vars === null) {
|
|
|
|
$if_scope->new_vars = array_diff_key($elseif_context->vars_in_scope, $outer_context->vars_in_scope);
|
|
|
|
} else {
|
|
|
|
foreach ($if_scope->new_vars as $new_var => $type) {
|
2017-02-01 05:24:33 +01:00
|
|
|
if (!$elseif_context->hasVariable($new_var)) {
|
2016-11-11 23:13:13 +01:00
|
|
|
unset($if_scope->new_vars[$new_var]);
|
|
|
|
} else {
|
|
|
|
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
|
|
|
|
$type,
|
|
|
|
$elseif_context->vars_in_scope[$new_var]
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($if_scope->redefined_vars === null) {
|
|
|
|
$if_scope->redefined_vars = $elseif_redefined_vars;
|
|
|
|
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
|
|
|
|
} else {
|
|
|
|
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
|
|
|
|
if (!isset($elseif_redefined_vars[$redefined_var])) {
|
|
|
|
unset($if_scope->redefined_vars[$redefined_var]);
|
|
|
|
} else {
|
|
|
|
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
|
|
|
|
$elseif_redefined_vars[$redefined_var],
|
|
|
|
$type
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($elseif_redefined_vars as $var => $type) {
|
|
|
|
if ($type->isMixed()) {
|
|
|
|
$if_scope->possibly_redefined_vars[$var] = $type;
|
|
|
|
} elseif (isset($if_scope->possibly_redefined_vars[$var])) {
|
|
|
|
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
|
|
|
|
$type,
|
|
|
|
$if_scope->possibly_redefined_vars[$var]
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$if_scope->possibly_redefined_vars[$var] = $type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-27 19:58:58 +01:00
|
|
|
if ($negated_elseif_types) {
|
2017-03-14 20:48:52 +01:00
|
|
|
if ($has_leaving_statements) {
|
|
|
|
$changed_vars = [];
|
|
|
|
|
|
|
|
$leaving_vars_reconciled = TypeChecker::reconcileKeyedTypes(
|
|
|
|
$negated_elseif_types,
|
|
|
|
$pre_conditional_context->vars_in_scope,
|
|
|
|
$changed_vars,
|
|
|
|
$statements_checker->getFileChecker(),
|
|
|
|
new CodeLocation($statements_checker->getSource(), $elseif),
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($leaving_vars_reconciled === false) {
|
|
|
|
return false;
|
2017-03-11 18:32:27 +01:00
|
|
|
}
|
|
|
|
|
2017-03-14 20:48:52 +01:00
|
|
|
$implied_outer_context = clone $elseif_context;
|
|
|
|
$implied_outer_context->vars_in_scope = $leaving_vars_reconciled;
|
|
|
|
|
|
|
|
$outer_context->update(
|
|
|
|
$elseif_context,
|
|
|
|
$implied_outer_context,
|
|
|
|
false,
|
|
|
|
array_keys($negated_elseif_types),
|
|
|
|
$if_scope->updated_vars
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$negated_keys = [];
|
|
|
|
|
|
|
|
$outer_context->update(
|
|
|
|
$old_elseif_context,
|
|
|
|
$elseif_context,
|
|
|
|
false,
|
|
|
|
array_keys($negated_elseif_types),
|
|
|
|
$if_scope->updated_vars
|
|
|
|
);
|
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!$has_ending_statements) {
|
|
|
|
$vars = array_diff_key($elseif_context->vars_possibly_in_scope, $outer_context->vars_possibly_in_scope);
|
|
|
|
|
|
|
|
// if we're leaving this block, add vars to outer for loop scope
|
|
|
|
if ($has_leaving_statements && $if_scope->loop_context) {
|
|
|
|
if ($if_scope->redefined_loop_vars === null) {
|
|
|
|
$if_scope->redefined_loop_vars = $elseif_redefined_vars;
|
|
|
|
$if_scope->possibly_redefined_loop_vars = $if_scope->redefined_loop_vars;
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-11-11 23:13:13 +01:00
|
|
|
foreach ($if_scope->redefined_loop_vars as $redefined_var => $type) {
|
2016-10-22 19:23:18 +02:00
|
|
|
if (!isset($elseif_redefined_vars[$redefined_var])) {
|
2016-11-11 23:13:13 +01:00
|
|
|
unset($if_scope->redefined_loop_vars[$redefined_var]);
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->redefined_loop_vars[$redefined_var] = Type::combineUnionTypes(
|
2016-11-02 07:29:00 +01:00
|
|
|
$elseif_redefined_vars[$redefined_var],
|
|
|
|
$type
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($elseif_redefined_vars as $var => $type) {
|
|
|
|
if ($type->isMixed()) {
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->possibly_redefined_loop_vars[$var] = $type;
|
|
|
|
} elseif (isset($if_scope->possibly_redefined_loop_vars[$var])) {
|
|
|
|
$if_scope->possibly_redefined_loop_vars[$var] = Type::combineUnionTypes(
|
2016-11-02 07:29:00 +01:00
|
|
|
$type,
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->possibly_redefined_loop_vars[$var]
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
|
|
|
} else {
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->possibly_redefined_loop_vars[$var] = $type;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->loop_context->vars_possibly_in_scope = array_merge(
|
|
|
|
$vars,
|
|
|
|
$if_scope->loop_context->vars_possibly_in_scope
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
2016-11-11 23:13:13 +01:00
|
|
|
} elseif (!$has_leaving_statements) {
|
|
|
|
$if_scope->new_vars_possibly_in_scope = array_merge($vars, $if_scope->new_vars_possibly_in_scope);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
|
|
|
}
|
2016-12-27 19:58:58 +01:00
|
|
|
|
2017-02-27 05:09:18 +01:00
|
|
|
if ($outer_context->collect_references) {
|
2017-02-01 05:24:33 +01:00
|
|
|
$outer_context->referenced_vars = array_merge(
|
|
|
|
$outer_context->referenced_vars,
|
|
|
|
$elseif_context->referenced_vars
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-03-16 19:45:45 +01:00
|
|
|
$if_scope->negated_clauses = array_merge(
|
|
|
|
$if_scope->negated_clauses,
|
2016-12-27 19:58:58 +01:00
|
|
|
TypeChecker::negateFormula($elseif_clauses)
|
|
|
|
);
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
/**
|
|
|
|
* @param StatementsChecker $statements_checker
|
|
|
|
* @param PhpParser\Node\Stmt\Else_ $else
|
|
|
|
* @param IfScope $if_scope
|
|
|
|
* @param Context $else_context
|
|
|
|
* @param Context $outer_context
|
|
|
|
* @return false|null
|
|
|
|
*/
|
2017-01-07 21:09:47 +01:00
|
|
|
protected static function analyzeElseBlock(
|
2016-11-11 23:13:13 +01:00
|
|
|
StatementsChecker $statements_checker,
|
|
|
|
PhpParser\Node\Stmt\Else_ $else,
|
|
|
|
IfScope $if_scope,
|
|
|
|
Context $else_context,
|
2017-03-16 19:45:45 +01:00
|
|
|
Context $outer_context
|
2016-11-11 23:13:13 +01:00
|
|
|
) {
|
|
|
|
$original_context = clone $else_context;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-12-27 19:58:58 +01:00
|
|
|
$else_context->clauses = TypeChecker::simplifyCNF(
|
|
|
|
array_merge(
|
|
|
|
$outer_context->clauses,
|
2017-03-16 19:45:45 +01:00
|
|
|
$if_scope->negated_clauses
|
2016-12-27 19:58:58 +01:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
$else_types = TypeChecker::getTruthsFromFormula($else_context->clauses);
|
|
|
|
|
|
|
|
if ($else_types) {
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars = [];
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$else_vars_reconciled = TypeChecker::reconcileKeyedTypes(
|
2016-12-27 19:58:58 +01:00
|
|
|
$else_types,
|
2016-11-11 23:13:13 +01:00
|
|
|
$else_context->vars_in_scope,
|
2016-12-28 20:20:16 +01:00
|
|
|
$changed_vars,
|
2017-01-02 21:31:18 +01:00
|
|
|
$statements_checker->getFileChecker(),
|
2016-12-04 01:11:30 +01:00
|
|
|
new CodeLocation($statements_checker->getSource(), $else),
|
2016-11-11 23:13:13 +01:00
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($else_vars_reconciled === false) {
|
|
|
|
return false;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$else_context->vars_in_scope = $else_vars_reconciled;
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$old_else_context = clone $else_context;
|
2016-11-02 07:29:00 +01:00
|
|
|
|
2017-01-07 21:09:47 +01:00
|
|
|
if ($statements_checker->analyze($else->stmts, $else_context, $if_scope->loop_context) === false) {
|
2016-11-11 23:13:13 +01:00
|
|
|
return false;
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2017-02-23 06:25:28 +01:00
|
|
|
if ($else_context->byref_constraints !== null) {
|
|
|
|
foreach ($else_context->byref_constraints as $var_id => $byref_constraint) {
|
|
|
|
if ($outer_context->byref_constraints !== null &&
|
|
|
|
isset($outer_context->byref_constraints[$var_id]) &&
|
|
|
|
!TypeChecker::isContainedBy(
|
|
|
|
$byref_constraint->type,
|
|
|
|
$outer_context->byref_constraints[$var_id]->type,
|
|
|
|
$statements_checker->getFileChecker()
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new ConflictingReferenceConstraint(
|
|
|
|
'There is more than one pass-by--reference constraint on ' . $var_id,
|
|
|
|
new CodeLocation($statements_checker, $else, true)
|
|
|
|
),
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$outer_context->byref_constraints[$var_id] = $byref_constraint;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-27 05:09:18 +01:00
|
|
|
if ($outer_context->collect_references) {
|
2017-02-02 06:45:23 +01:00
|
|
|
$outer_context->referenced_vars = array_merge(
|
|
|
|
$outer_context->referenced_vars,
|
|
|
|
$else_context->referenced_vars
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if (count($else->stmts)) {
|
|
|
|
// has a return/throw at end
|
|
|
|
$has_ending_statements = ScopeChecker::doesAlwaysReturnOrThrow($else->stmts);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$has_leaving_statements = $has_ending_statements ||
|
|
|
|
ScopeChecker::doesAlwaysBreakOrContinue($else->stmts);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$else_redefined_vars = Context::getRedefinedVars($original_context, $else_context);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
// if it doesn't end in a return
|
|
|
|
if (!$has_leaving_statements) {
|
|
|
|
if ($if_scope->new_vars === null) {
|
|
|
|
$if_scope->new_vars = array_diff_key($else_context->vars_in_scope, $outer_context->vars_in_scope);
|
|
|
|
} else {
|
|
|
|
foreach ($if_scope->new_vars as $new_var => $type) {
|
2017-02-01 05:24:33 +01:00
|
|
|
if (!$else_context->hasVariable($new_var)) {
|
2016-11-11 23:13:13 +01:00
|
|
|
unset($if_scope->new_vars[$new_var]);
|
|
|
|
} else {
|
|
|
|
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
|
|
|
|
$type,
|
|
|
|
$else_context->vars_in_scope[$new_var]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
if ($if_scope->redefined_vars === null) {
|
|
|
|
$if_scope->redefined_vars = $else_redefined_vars;
|
|
|
|
$if_scope->possibly_redefined_vars = $if_scope->redefined_vars;
|
|
|
|
} else {
|
|
|
|
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
|
|
|
|
if (!isset($else_redefined_vars[$redefined_var])) {
|
|
|
|
unset($if_scope->redefined_vars[$redefined_var]);
|
|
|
|
} else {
|
|
|
|
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
|
|
|
|
$else_redefined_vars[$redefined_var],
|
|
|
|
$type
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
foreach ($else_redefined_vars as $var => $type) {
|
|
|
|
if ($type->isMixed()) {
|
|
|
|
$if_scope->possibly_redefined_vars[$var] = $type;
|
|
|
|
} elseif (isset($if_scope->possibly_redefined_vars[$var])) {
|
|
|
|
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
|
|
|
|
$type,
|
|
|
|
$if_scope->possibly_redefined_vars[$var]
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$if_scope->possibly_redefined_vars[$var] = $type;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2017-03-16 19:45:45 +01:00
|
|
|
} elseif (!$if_scope->has_elseifs) {
|
|
|
|
$outer_context->clauses = TypeChecker::simplifyCNF(
|
|
|
|
array_merge(
|
|
|
|
$if_scope->reasonable_clauses,
|
|
|
|
$original_context->clauses
|
|
|
|
)
|
|
|
|
);
|
2016-11-11 23:13:13 +01:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
// update the parent context as necessary
|
|
|
|
if ($if_scope->negatable_if_types) {
|
|
|
|
$outer_context->update(
|
|
|
|
$old_else_context,
|
|
|
|
$else_context,
|
|
|
|
$has_leaving_statements,
|
|
|
|
array_keys($if_scope->negatable_if_types),
|
|
|
|
$if_scope->updated_vars
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$has_ending_statements) {
|
|
|
|
$vars = array_diff_key($else_context->vars_possibly_in_scope, $outer_context->vars_possibly_in_scope);
|
|
|
|
|
|
|
|
if ($has_leaving_statements && $if_scope->loop_context) {
|
|
|
|
if ($if_scope->redefined_loop_vars === null) {
|
|
|
|
$if_scope->redefined_loop_vars = $else_redefined_vars;
|
|
|
|
$if_scope->possibly_redefined_loop_vars = $if_scope->redefined_loop_vars;
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-11-11 23:13:13 +01:00
|
|
|
foreach ($if_scope->redefined_loop_vars as $redefined_var => $type) {
|
2016-10-22 19:23:18 +02:00
|
|
|
if (!isset($else_redefined_vars[$redefined_var])) {
|
2016-11-11 23:13:13 +01:00
|
|
|
unset($if_scope->redefined_loop_vars[$redefined_var]);
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->redefined_loop_vars[$redefined_var] = Type::combineUnionTypes(
|
2016-11-02 07:29:00 +01:00
|
|
|
$else_redefined_vars[$redefined_var],
|
|
|
|
$type
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($else_redefined_vars as $var => $type) {
|
|
|
|
if ($type->isMixed()) {
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->possibly_redefined_loop_vars[$var] = $type;
|
|
|
|
} elseif (isset($if_scope->possibly_redefined_loop_vars[$var])) {
|
|
|
|
$if_scope->possibly_redefined_loop_vars[$var] = Type::combineUnionTypes(
|
2016-11-02 07:29:00 +01:00
|
|
|
$type,
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->possibly_redefined_loop_vars[$var]
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
|
|
|
} else {
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->possibly_redefined_loop_vars[$var] = $type;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-11 23:13:13 +01:00
|
|
|
$if_scope->loop_context->vars_possibly_in_scope = array_merge(
|
|
|
|
$vars,
|
|
|
|
$if_scope->loop_context->vars_possibly_in_scope
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
2016-11-11 23:13:13 +01:00
|
|
|
} elseif (!$has_leaving_statements) {
|
|
|
|
$if_scope->new_vars_possibly_in_scope = array_merge($vars, $if_scope->new_vars_possibly_in_scope);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Expr $stmt
|
|
|
|
* @return PhpParser\Node\Expr|null
|
|
|
|
*/
|
2016-12-28 23:04:03 +01:00
|
|
|
protected static function getDefinitelyEvaluatedExpression(PhpParser\Node\Expr $stmt)
|
2016-10-22 19:23:18 +02:00
|
|
|
{
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
|
2016-12-28 23:04:03 +01:00
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd ||
|
2017-02-17 03:00:45 +01:00
|
|
|
$stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd ||
|
2016-12-28 23:04:03 +01:00
|
|
|
$stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor
|
|
|
|
) {
|
|
|
|
return self::getDefinitelyEvaluatedExpression($stmt->left);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $stmt;
|
|
|
|
}
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BooleanNot) {
|
2016-12-28 23:04:03 +01:00
|
|
|
return self::getDefinitelyEvaluatedExpression($stmt->expr);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
|
2017-01-07 20:35:07 +01:00
|
|
|
return $stmt;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
|
|
|
}
|