1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Add support for infinite loops

Fixes #381
This commit is contained in:
Matthew Brown 2017-12-06 23:46:41 -05:00
parent 6c84fecce9
commit 2a7b48ce5f
3 changed files with 114 additions and 10 deletions

View File

@ -2,6 +2,7 @@
namespace Psalm\Checker\Statements\Block; namespace Psalm\Checker\Statements\Block;
use PhpParser; use PhpParser;
use Psalm\Checker\ScopeChecker;
use Psalm\Checker\Statements\ExpressionChecker; use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\StatementsChecker; use Psalm\Checker\StatementsChecker;
use Psalm\Context; use Psalm\Context;
@ -27,21 +28,50 @@ class ForChecker
} }
} }
$while_true = !$stmt->cond && !$stmt->init && !$stmt->loop;
$pre_context = $while_true ? clone $context : null;
$for_context = clone $context; $for_context = clone $context;
$for_context->inside_loop = true; $for_context->inside_loop = true;
$loop_scope = new LoopScope($for_context, $context);
LoopChecker::analyze( LoopChecker::analyze(
$statements_checker, $statements_checker,
$stmt->stmts, $stmt->stmts,
$stmt->cond, $stmt->cond,
$stmt->loop, $stmt->loop,
new LoopScope($for_context, $context) $loop_scope,
$inner_loop_context
); );
if ($inner_loop_context && $while_true) {
// if we actually leave the loop
if (in_array(ScopeChecker::ACTION_BREAK, $loop_scope->final_actions, true)
|| in_array(ScopeChecker::ACTION_END, $loop_scope->final_actions, true)
) {
foreach ($inner_loop_context->vars_in_scope as $var_id => $type) {
if (!isset($context->vars_in_scope[$var_id])) {
$context->vars_in_scope[$var_id] = $type;
}
}
}
}
if (!$while_true
|| in_array(ScopeChecker::ACTION_BREAK, $loop_scope->final_actions, true)
|| in_array(ScopeChecker::ACTION_END, $loop_scope->final_actions, true)
|| !$pre_context
) {
$context->vars_possibly_in_scope = array_merge( $context->vars_possibly_in_scope = array_merge(
$for_context->vars_possibly_in_scope, $context->vars_possibly_in_scope,
$context->vars_possibly_in_scope $for_context->vars_possibly_in_scope
); );
} else {
$context->vars_in_scope = $pre_context->vars_in_scope;
$context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope;
}
$context->referenced_var_ids = array_merge( $context->referenced_var_ids = array_merge(
$for_context->referenced_var_ids, $for_context->referenced_var_ids,

View File

@ -2,6 +2,7 @@
namespace Psalm\Checker\Statements\Block; namespace Psalm\Checker\Statements\Block;
use PhpParser; use PhpParser;
use Psalm\Checker\ScopeChecker;
use Psalm\Checker\StatementsChecker; use Psalm\Checker\StatementsChecker;
use Psalm\Context; use Psalm\Context;
use Psalm\Scope\LoopScope; use Psalm\Scope\LoopScope;
@ -20,20 +21,55 @@ class WhileChecker
PhpParser\Node\Stmt\While_ $stmt, PhpParser\Node\Stmt\While_ $stmt,
Context $context Context $context
) { ) {
$while_true = $stmt->cond
&& ($stmt->cond instanceof PhpParser\Node\Expr\ConstFetch && $stmt->cond->name->parts === ['true'])
|| ($stmt->cond instanceof PhpParser\Node\Scalar\LNumber && $stmt->cond->value > 0);
$pre_context = null;
if ($while_true) {
$pre_context = clone $context;
}
$while_context = clone $context; $while_context = clone $context;
$loop_scope = new LoopScope($while_context, $context);
LoopChecker::analyze( LoopChecker::analyze(
$statements_checker, $statements_checker,
$stmt->stmts, $stmt->stmts,
$stmt->cond ? [$stmt->cond] : [], $stmt->cond ? [$stmt->cond] : [],
[], [],
new LoopScope($while_context, $context) $loop_scope,
$inner_loop_context
); );
if ($inner_loop_context && $while_true) {
// if we actually leave the loop
if (in_array(ScopeChecker::ACTION_BREAK, $loop_scope->final_actions, true)
|| in_array(ScopeChecker::ACTION_END, $loop_scope->final_actions, true)
) {
foreach ($inner_loop_context->vars_in_scope as $var_id => $type) {
if (!isset($context->vars_in_scope[$var_id])) {
$context->vars_in_scope[$var_id] = $type;
}
}
}
}
if (!$while_true
|| in_array(ScopeChecker::ACTION_BREAK, $loop_scope->final_actions, true)
|| in_array(ScopeChecker::ACTION_END, $loop_scope->final_actions, true)
|| !$pre_context
) {
$context->vars_possibly_in_scope = array_merge( $context->vars_possibly_in_scope = array_merge(
$context->vars_possibly_in_scope, $context->vars_possibly_in_scope,
$while_context->vars_possibly_in_scope $while_context->vars_possibly_in_scope
); );
} else {
$context->vars_in_scope = $pre_context->vars_in_scope;
$context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope;
}
$context->referenced_var_ids = array_merge( $context->referenced_var_ids = array_merge(
$context->referenced_var_ids, $context->referenced_var_ids,

View File

@ -764,6 +764,26 @@ class LoopScopeTest extends TestCase
'MixedAssignment', 'MixedArrayAccess', 'MixedAssignment', 'MixedArrayAccess',
], ],
], ],
'whileTrue' => [
'<?php
while (true) {
$a = "hello";
break;
}
while (1) {
$b = 5;
break;
}
for(;;) {
$c = true;
break;
}',
'assignments' => [
'$a' => 'string',
'$b' => 'int',
'$c' => 'bool',
],
],
]; ];
} }
@ -898,6 +918,24 @@ class LoopScopeTest extends TestCase
}', }',
'error_message' => 'RedundantCondition', 'error_message' => 'RedundantCondition',
], ],
'whileTrueNoBreak' => [
'<?php
while (true) {
$a = "hello";
}
echo $a;',
'error_message' => 'UndefinedGlobalVariable',
],
'forInfiniteNoBreak' => [
'<?php
for (;;) {
$a = "hello";
}
echo $a;',
'error_message' => 'UndefinedGlobalVariable',
],
]; ];
} }
} }