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

Add better scope analysis

This commit is contained in:
Matthew Brown 2016-06-20 16:18:31 -04:00
parent 953ba22a38
commit a6eed85ba5
6 changed files with 192 additions and 25 deletions

View File

@ -194,4 +194,9 @@ class Config
{
return $this->plugins;
}
public function setIssueHandler($issue_name, FileFilter $filter = null)
{
$this->issue_handlers[$issue_name] = $filter;
}
}

View File

@ -93,10 +93,10 @@ class FileFilter
if (strpos($file_name, $exclude_dir) === 0) {
return false;
}
}
if (in_array($file_name, $this->exclude_files)) {
return false;
}
if (in_array($file_name, $this->exclude_files)) {
return false;
}
return true;
@ -121,4 +121,14 @@ class FileFilter
{
return $this->exclude_files;
}
public function makeExclusive()
{
$this->inclusive = false;
}
public function addExcludeFile($file_name)
{
$this->exclude_files[] = $file_name;
}
}

View File

@ -77,7 +77,7 @@ class EffectsAnalyser
}
// if we're at the top level and we're not ending in a return, make sure to add possible null
if ($collapse_types && !$last_stmt instanceof PhpParser\Node\Stmt\Return_ && !ScopeChecker::doesLeaveBlock($stmts, false, false)) {
if ($collapse_types && !$last_stmt instanceof PhpParser\Node\Stmt\Return_ && !ScopeChecker::doesReturnOrThrow($stmts)) {
$return_types[] = Type::getNull(false);
}

View File

@ -31,7 +31,8 @@ class ScopeChecker
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
if ($stmt->else && self::doesLeaveBlock($stmt->stmts, $check_continue, $check_break) &&
if ($stmt->else &&
self::doesLeaveBlock($stmt->stmts, $check_continue, $check_break) &&
self::doesLeaveBlock($stmt->else->stmts, $check_continue, $check_break)) {
if (empty($stmt->elseifs)) {
@ -70,6 +71,121 @@ class ScopeChecker
return false;
}
public static function doesBreakOrContinue(array $stmts)
{
if (empty($stmts)) {
return false;
}
for ($i = count($stmts) - 1; $i >= 0; $i--) {
$stmt = $stmts[$i];
if ($stmt instanceof PhpParser\Node\Stmt\Continue_ || $stmt instanceof PhpParser\Node\Stmt\Break_) {
return true;
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
if ($stmt->else && self::doesBreakOrContinue($stmt->stmts) && self::doesBreakOrContinue($stmt->else->stmts)) {
if (empty($stmt->elseifs)) {
return true;
}
foreach ($stmt->elseifs as $elseif) {
if (!self::doesBreakOrContinue($elseif->stmts)) {
return false;
}
}
return true;
}
}
if ($stmt instanceof PhpParser\Node\Stmt\Switch_ && $stmt->cases[count($stmt->cases) - 1]->cond === null) {
$all_cases_terminate = true;
foreach ($stmt->cases as $case) {
if (!self::doesBreakOrContinue($case->stmts)) {
return false;
}
}
return true;
}
if ($stmt instanceof PhpParser\Node\Stmt\Nop) {
continue;
}
return false;
}
return false;
}
public static function doesReturnOrThrow(array $stmts)
{
if (empty($stmts)) {
return false;
}
for ($i = count($stmts) - 1; $i >= 0; $i--) {
$stmt = $stmts[$i];
if ($stmt instanceof PhpParser\Node\Stmt\Return_ || $stmt instanceof PhpParser\Node\Stmt\Throw_) {
return true;
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
if ($stmt->else && self::doesReturnOrThrow($stmt->stmts) && self::doesReturnOrThrow($stmt->else->stmts)) {
if (empty($stmt->elseifs)) {
return true;
}
foreach ($stmt->elseifs as $elseif) {
if (!self::doesReturnOrThrow($elseif->stmts)) {
return false;
}
}
return true;
}
}
if ($stmt instanceof PhpParser\Node\Stmt\Switch_ && $stmt->cases[count($stmt->cases) - 1]->cond === null) {
$all_cases_terminate = true;
$has_default = false;
foreach ($stmt->cases as $case) {
if (self::doesBreakOrContinue($case->stmts)) {
return false;
}
if (self::doesReturnOrThrow($case->stmts)) {
return true;
}
if (!$case->cond) {
$has_default = true;
}
}
if ($has_default) {
return false;
}
return true;
}
if ($stmt instanceof PhpParser\Node\Stmt\Nop) {
continue;
}
return false;
}
return false;
}
public static function onlyThrows(array $stmts)
{
if (empty($stmts)) {

View File

@ -291,6 +291,7 @@ class StatementsChecker
}
$old_if_context = clone $if_context;
$context->vars_possibly_in_scope = array_merge($if_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
if ($this->check($stmt->stmts, $if_context, $for_vars_possibly_in_scope) === false) {
return false;
@ -326,7 +327,7 @@ class StatementsChecker
$context->update($old_if_context, $if_context, $has_leaving_statments, $updated_vars);
}
$has_ending_statments = ScopeChecker::doesLeaveBlock($stmt->stmts, false, false);
$has_ending_statments = ScopeChecker::doesReturnOrThrow($stmt->stmts);
if (!$has_ending_statments) {
$vars = array_diff_key($if_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
@ -438,7 +439,7 @@ class StatementsChecker
}
// has a return/throw at end
$has_ending_statments = ScopeChecker::doesLeaveBlock($elseif->stmts, false, false);
$has_ending_statments = ScopeChecker::doesReturnOrThrow($elseif->stmts);
if (!$has_ending_statments) {
$vars = array_diff_key($elseif_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
@ -526,7 +527,7 @@ class StatementsChecker
}
// has a return/throw at end
$has_ending_statments = ScopeChecker::doesLeaveBlock($stmt->else->stmts, false, false);
$has_ending_statments = ScopeChecker::doesReturnOrThrow($stmt->else->stmts);
if (!$has_ending_statments) {
$vars = array_diff_key($else_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
@ -1139,9 +1140,12 @@ class StatementsChecker
protected function _checkTryCatch(PhpParser\Node\Stmt\TryCatch $stmt, Context $context)
{
$original_context = clone $context;
$this->check($stmt->stmts, $context);
// clone context for catches after running the try block, as
// we optimistically assume it only failed at the very end
$original_context = clone $context;
foreach ($stmt->catches as $catch) {
$catch_context = clone $original_context;
@ -1166,10 +1170,14 @@ class StatementsChecker
$this->check($catch->stmts, $catch_context);
foreach ($catch_context->vars_in_scope as $catch_var => $type) {
if ($catch->var !== $catch_var && isset($context->vars_in_scope[$catch_var]) && (string) $context->vars_in_scope[$catch_var] !== (string) $type) {
$context->vars_in_scope[$catch_var] = Type::combineUnionTypes($context->vars_in_scope[$catch_var], $type);
if (!ScopeChecker::doesReturnOrThrow($catch->stmts, false, false)) {
foreach ($catch_context->vars_in_scope as $catch_var => $type) {
if ($catch->var !== $catch_var && isset($context->vars_in_scope[$catch_var]) && (string) $context->vars_in_scope[$catch_var] !== (string) $type) {
$context->vars_in_scope[$catch_var] = Type::combineUnionTypes($context->vars_in_scope[$catch_var], $type);
}
}
$context->vars_possibly_in_scope = array_merge($catch_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
}
}
@ -1341,21 +1349,22 @@ class StatementsChecker
$context->vars_in_scope[$var] = $while_context->vars_in_scope[$var];
}
if ($while_context->vars_in_scope[$var] !== $type) {
$context->vars_in_scope[$var]->types = array_merge($context->vars_in_scope[$var]->types, $while_context->vars_in_scope[$var]->types);
if ((string) $while_context->vars_in_scope[$var] !== (string) $type) {
$context->vars_in_scope[$var] = Type::combineUnionTypes($while_context->vars_in_scope[$var], $type);
}
}
$context->vars_possibly_in_scope = array_merge($context->vars_possibly_in_scope, $while_context->vars_possibly_in_scope);
}
protected function _checkDo(PhpParser\Node\Stmt\Do_ $stmt, Context $context)
{
$do_context = clone $context;
if ($this->check($stmt->stmts, $do_context) === false) {
// do not clone context for do, because it executes in current scope always
if ($this->check($stmt->stmts, $context) === false) {
return false;
}
return $this->_checkCondition($stmt->cond, $do_context);
return $this->_checkCondition($stmt->cond, $context);
}
protected function _checkBinaryOp(PhpParser\Node\Expr\BinaryOp $stmt, Context $context, $nesting = 0)
@ -1378,12 +1387,33 @@ class StatementsChecker
return false;
}
$new_context = clone $context;
$new_context->vars_in_scope = $op_vars_in_scope;
$op_context = clone $context;
$op_context->vars_in_scope = $op_vars_in_scope;
if ($this->_checkExpression($stmt->right, $new_context) === false) {
if ($this->_checkExpression($stmt->right, $op_context) === false) {
return false;
}
foreach ($op_context->vars_in_scope as $var => $type) {
if (!isset($context->vars_in_scope[$var])) {
$context->vars_in_scope[$var] = $type;
continue;
}
if ($type->isMixed()) {
continue;
}
if ($context->vars_in_scope[$var]->isMixed()) {
$context->vars_in_scope[$var] = $op_context->vars_in_scope[$var];
}
if ((string) $op_context->vars_in_scope[$var] !== (string) $type) {
$context->vars_in_scope[$var] = Type::combineUnionTypes($context->vars_in_scope[$var], $op_context->vars_in_scope[$var]);
}
}
$context->vars_possibly_in_scope = array_merge($op_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
}
else if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
$left_type_assertions = $this->_type_checker->getTypeAssertions($stmt->left, true);
@ -1402,12 +1432,14 @@ class StatementsChecker
return false;
}
$new_context = clone $context;
$new_context->vars_in_scope = $op_vars_in_scope;
$op_context = clone $context;
$op_context->vars_in_scope = $op_vars_in_scope;
if ($this->_checkExpression($stmt->right, $new_context) === false) {
if ($this->_checkExpression($stmt->right, $op_context) === false) {
return false;
}
$context->vars_possibly_in_scope = array_merge($op_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);
}
else {
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
@ -2447,7 +2479,7 @@ class StatementsChecker
$last_stmt = $case->stmts[count($case->stmts) - 1];
// has a return/throw at end
$has_ending_statments = ScopeChecker::doesLeaveBlock($case->stmts, false, false);
$has_ending_statments = ScopeChecker::doesReturnOrThrow($case->stmts);
if (!$has_ending_statments) {
$vars = array_diff_key($case_context->vars_possibly_in_scope, $context->vars_possibly_in_scope);

View File

@ -65,6 +65,10 @@ class TypeChecker
else if ($var_name = StatementsChecker::getVarId($conditional->expr)) {
$if_types[$var_name] = 'empty';
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\Assign) {
$var_name = StatementsChecker::getVarId($conditional->expr->var);
$if_types[$var_name] = 'empty';
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical || $conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Equal) {
$null_position = self::_hasNullVariable($conditional->expr);
$false_position = self::_hasNullVariable($conditional->expr);