1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-16 03:17:02 +01:00
psalm/src/Psalm/Checker/Statements/Block/TryChecker.php
Tyson Andre aea3779c96 Fix unused imports in psalm, wrong param order doc comments (#523)
(Some of the imports appear as strings elsewhere in the same file)
2018-02-18 17:55:11 -05:00

307 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Psalm\Checker\Statements\Block;
use PhpParser;
use Psalm\Checker\ClassLikeChecker;
use Psalm\Checker\ScopeChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\InvalidCatch;
use Psalm\IssueBuffer;
use Psalm\Scope\LoopScope;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
class TryChecker
{
/**
* @param StatementsChecker $statements_checker
* @param PhpParser\Node\Stmt\TryCatch $stmt
* @param Context $context
*
* @return false|null
*/
public static function analyze(
StatementsChecker $statements_checker,
PhpParser\Node\Stmt\TryCatch $stmt,
Context $context,
LoopScope $loop_scope = null
) {
$catch_actions = [];
$all_catches_leave = true;
/** @var int $i */
foreach ($stmt->catches as $i => $catch) {
$catch_actions[$i] = ScopeChecker::getFinalControlActions($catch->stmts);
$all_catches_leave = $all_catches_leave && !in_array(ScopeChecker::ACTION_NONE, $catch_actions[$i], true);
}
$project_checker = $statements_checker->getFileChecker()->project_checker;
$codebase = $project_checker->codebase;
if ($all_catches_leave) {
$try_context = $context;
} else {
$try_context = clone $context;
if ($project_checker->alter_code) {
$try_context->branch_point = $try_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
}
}
$assigned_var_ids = $context->assigned_var_ids;
$context->assigned_var_ids = [];
$old_unreferenced_vars = $try_context->unreferenced_vars;
$newly_unreferenced_vars = [];
$reassigned_vars = [];
if ($statements_checker->analyze($stmt->stmts, $context, $loop_scope) === false) {
return false;
}
$context->assigned_var_ids = $assigned_var_ids;
if ($try_context !== $context) {
foreach ($context->vars_in_scope as $var_id => $type) {
if (!isset($try_context->vars_in_scope[$var_id])) {
$try_context->vars_in_scope[$var_id] = clone $type;
$try_context->vars_in_scope[$var_id]->from_docblock = true;
} else {
$try_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
$try_context->vars_in_scope[$var_id],
$type
);
}
}
$try_context->vars_possibly_in_scope = $context->vars_possibly_in_scope;
$context->referenced_var_ids = array_merge(
$try_context->referenced_var_ids,
$context->referenced_var_ids
);
if ($context->collect_references) {
$newly_unreferenced_vars = array_merge(
$newly_unreferenced_vars,
array_diff_key(
$context->unreferenced_vars,
$old_unreferenced_vars
)
);
foreach ($context->unreferenced_vars as $var_id => $location) {
if (isset($old_unreferenced_vars[$var_id])
&& $old_unreferenced_vars[$var_id] !== $location
) {
$reassigned_vars[$var_id] = $location;
}
}
}
}
$try_leaves_loop = $loop_scope
&& $loop_scope->final_actions
&& !in_array(ScopeChecker::ACTION_NONE, $loop_scope->final_actions, true);
if (!$all_catches_leave) {
foreach ($assigned_var_ids as $assigned_var_id => $_) {
$context->removeVarFromConflictingClauses($assigned_var_id);
}
} else {
foreach ($assigned_var_ids as $assigned_var_id => $_) {
$try_context->removeVarFromConflictingClauses($assigned_var_id);
}
}
// at this point we have two contexts $context, in which it is assumed that everything was fine,
// and $try_context - which allows all variables to have the union of the values before and after
// the try was applied
$original_context = clone $try_context;
/** @var int $i */
foreach ($stmt->catches as $i => $catch) {
$catch_context = clone $original_context;
$fq_catch_classes = [];
foreach ($catch->types as $catch_type) {
$fq_catch_class = ClassLikeChecker::getFQCLNFromNameObject(
$catch_type,
$statements_checker->getAliases()
);
if ($original_context->check_classes) {
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$statements_checker,
$fq_catch_class,
new CodeLocation($statements_checker->getSource(), $catch_type, $context->include_location),
$statements_checker->getSuppressedIssues(),
false
) === false) {
return false;
}
}
if (($codebase->classExists($fq_catch_class)
&& strtolower($fq_catch_class) !== 'exception'
&& !($codebase->classExtends($fq_catch_class, 'Exception')
|| $codebase->classImplements($fq_catch_class, 'Throwable')))
|| ($codebase->interfaceExists($fq_catch_class)
&& strtolower($fq_catch_class) !== 'throwable'
&& !$codebase->interfaceExtends($fq_catch_class, 'Throwable'))
) {
if (IssueBuffer::accepts(
new InvalidCatch(
'Class/interface ' . $fq_catch_class . ' cannot be caught',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
$fq_catch_classes[] = $fq_catch_class;
}
$catch_var_id = '$' . $catch->var;
$catch_context->vars_in_scope[$catch_var_id] = new Union(
array_map(
/**
* @param string $fq_catch_class
*
* @return Type\Atomic
*/
function ($fq_catch_class) use ($codebase) {
$catch_class_type = new TNamedObject($fq_catch_class);
if (version_compare(PHP_VERSION, '7.0.0dev', '>=')
&& $codebase->interfaceExists($fq_catch_class)
&& !$codebase->interfaceExtends($fq_catch_class, 'Throwable')
) {
$catch_class_type->addIntersectionType(new TNamedObject('Throwable'));
}
return $catch_class_type;
},
$fq_catch_classes
)
);
// discard all clauses because crazy stuff may have happened in try block
$catch_context->clauses = [];
$catch_context->vars_possibly_in_scope[$catch_var_id] = true;
if (!$statements_checker->hasVariable($catch_var_id)) {
$location = new CodeLocation(
$statements_checker,
$catch,
$context->include_location,
true,
CodeLocation::CATCH_VAR
);
$statements_checker->registerVariable(
$catch_var_id,
$location,
$try_context->branch_point
);
$catch_context->unreferenced_vars[$catch_var_id] = $location;
}
// this registers the variable to avoid unfair deadcode issues
$catch_context->hasVariable($catch_var_id, $statements_checker);
$suppressed_issues = $statements_checker->getSuppressedIssues();
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
$statements_checker->addSuppressedIssues(['RedundantCondition']);
}
$statements_checker->analyze($catch->stmts, $catch_context, $loop_scope);
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
$statements_checker->removeSuppressedIssues(['RedundantCondition']);
}
$context->referenced_var_ids = array_merge(
$catch_context->referenced_var_ids,
$context->referenced_var_ids
);
if ($context->collect_references) {
foreach ($context->unreferenced_vars as $var_id => $_) {
if (!isset($catch_context->unreferenced_vars[$var_id])) {
unset($context->unreferenced_vars[$var_id]);
}
}
$newly_unreferenced_vars = array_merge(
$newly_unreferenced_vars,
array_diff_key(
$catch_context->unreferenced_vars,
$old_unreferenced_vars
)
);
foreach ($catch_context->unreferenced_vars as $var_id => $location) {
if (!isset($old_unreferenced_vars[$var_id])
&& isset($context->unreferenced_vars[$var_id])
) {
$statements_checker->registerVariableUse($location);
} elseif (isset($old_unreferenced_vars[$var_id])
&& $old_unreferenced_vars[$var_id] !== $location
) {
$statements_checker->registerVariableUse($location);
}
}
}
if ($catch_actions[$i] !== [ScopeChecker::ACTION_END]) {
foreach ($catch_context->vars_in_scope as $var_id => $type) {
if ($catch->var !== $var_id &&
$context->hasVariable($var_id) &&
$context->vars_in_scope[$var_id]->getId() !== $type->getId()
) {
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
$context->vars_in_scope[$var_id],
$type
);
}
}
$context->vars_possibly_in_scope = array_merge(
$catch_context->vars_possibly_in_scope,
$context->vars_possibly_in_scope
);
}
}
if ($loop_scope
&& !$try_leaves_loop
&& !in_array(ScopeChecker::ACTION_NONE, $loop_scope->final_actions, true)
) {
$loop_scope->final_actions[] = ScopeChecker::ACTION_NONE;
}
if ($stmt->finally) {
$statements_checker->analyze($stmt->finally->stmts, $context, $loop_scope);
}
if ($context->collect_references) {
foreach ($old_unreferenced_vars as $var_id => $location) {
if (isset($context->unreferenced_vars[$var_id]) && $context->unreferenced_vars[$var_id] !== $location) {
$statements_checker->registerVariableUse($location);
}
}
}
return null;
}
}