2016-10-22 23:35:59 +02:00
|
|
|
|
<?php
|
|
|
|
|
namespace Psalm\Checker\Statements\Block;
|
|
|
|
|
|
|
|
|
|
use PhpParser;
|
|
|
|
|
use Psalm\Checker\ClassLikeChecker;
|
|
|
|
|
use Psalm\Checker\ScopeChecker;
|
|
|
|
|
use Psalm\Checker\StatementsChecker;
|
2017-05-19 06:48:26 +02:00
|
|
|
|
use Psalm\CodeLocation;
|
2016-11-02 07:29:00 +01:00
|
|
|
|
use Psalm\Context;
|
2017-12-28 20:40:28 +01:00
|
|
|
|
use Psalm\Issue\InvalidCatch;
|
|
|
|
|
use Psalm\IssueBuffer;
|
2017-12-03 00:28:18 +01:00
|
|
|
|
use Psalm\Scope\LoopScope;
|
2016-10-22 23:35:59 +02:00
|
|
|
|
use Psalm\Type;
|
2017-01-15 01:06:58 +01:00
|
|
|
|
use Psalm\Type\Atomic\TNamedObject;
|
2017-12-28 20:40:28 +01:00
|
|
|
|
use Psalm\Type\Union;
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
|
|
|
|
class TryChecker
|
|
|
|
|
{
|
|
|
|
|
/**
|
2016-11-02 07:29:00 +01:00
|
|
|
|
* @param StatementsChecker $statements_checker
|
|
|
|
|
* @param PhpParser\Node\Stmt\TryCatch $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 23:35:59 +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\TryCatch $stmt,
|
|
|
|
|
Context $context,
|
2017-12-03 00:28:18 +01:00
|
|
|
|
LoopScope $loop_scope = null
|
2016-11-02 07:29:00 +01:00
|
|
|
|
) {
|
2017-12-10 23:36:33 +01:00
|
|
|
|
$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);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 22:24:20 +01:00
|
|
|
|
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
2018-02-01 06:50:01 +01:00
|
|
|
|
$codebase = $project_checker->codebase;
|
2018-01-21 22:24:20 +01:00
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
if ($all_catches_leave) {
|
|
|
|
|
$try_context = $context;
|
|
|
|
|
} else {
|
|
|
|
|
$try_context = clone $context;
|
2018-01-21 22:24:20 +01:00
|
|
|
|
|
|
|
|
|
if ($project_checker->alter_code) {
|
|
|
|
|
$try_context->branch_point = $try_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
|
|
|
|
}
|
2017-12-10 23:36:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-27 17:23:54 +01:00
|
|
|
|
$assigned_var_ids = $try_context->assigned_var_ids;
|
2017-12-10 23:36:33 +01:00
|
|
|
|
$context->assigned_var_ids = [];
|
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
|
$old_unreferenced_vars = $try_context->unreferenced_vars;
|
|
|
|
|
$newly_unreferenced_vars = [];
|
|
|
|
|
$reassigned_vars = [];
|
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
if ($statements_checker->analyze($stmt->stmts, $context, $loop_scope) === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-27 17:23:54 +01:00
|
|
|
|
/** @var array<string, bool> */
|
|
|
|
|
$newly_assigned_var_ids = $context->assigned_var_ids;
|
|
|
|
|
|
|
|
|
|
$context->assigned_var_ids = array_merge(
|
|
|
|
|
$assigned_var_ids,
|
|
|
|
|
$newly_assigned_var_ids
|
|
|
|
|
);
|
2017-12-10 23:36:33 +01:00
|
|
|
|
|
|
|
|
|
if ($try_context !== $context) {
|
2017-12-11 00:09:38 +01:00
|
|
|
|
foreach ($context->vars_in_scope as $var_id => $type) {
|
|
|
|
|
if (!isset($try_context->vars_in_scope[$var_id])) {
|
2017-12-10 23:36:33 +01:00
|
|
|
|
$try_context->vars_in_scope[$var_id] = clone $type;
|
2017-12-11 00:09:38 +01:00
|
|
|
|
$try_context->vars_in_scope[$var_id]->from_docblock = true;
|
2017-12-10 23:36:33 +01:00
|
|
|
|
} else {
|
|
|
|
|
$try_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
2017-12-11 00:09:38 +01:00
|
|
|
|
$try_context->vars_in_scope[$var_id],
|
2017-12-10 23:36:33 +01:00
|
|
|
|
$type
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-12-11 00:34:22 +01:00
|
|
|
|
|
|
|
|
|
$try_context->vars_possibly_in_scope = $context->vars_possibly_in_scope;
|
2018-01-25 07:04:26 +01:00
|
|
|
|
|
|
|
|
|
$context->referenced_var_ids = array_merge(
|
|
|
|
|
$try_context->referenced_var_ids,
|
|
|
|
|
$context->referenced_var_ids
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($context->collect_references) {
|
2018-01-28 23:28:34 +01:00
|
|
|
|
$newly_unreferenced_vars = array_merge(
|
|
|
|
|
$newly_unreferenced_vars,
|
|
|
|
|
array_diff_key(
|
|
|
|
|
$context->unreferenced_vars,
|
|
|
|
|
$old_unreferenced_vars
|
|
|
|
|
)
|
2018-01-25 07:04:26 +01:00
|
|
|
|
);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
|
2018-06-17 02:01:33 +02:00
|
|
|
|
foreach ($context->unreferenced_vars as $var_id => $locations) {
|
2018-01-28 23:28:34 +01:00
|
|
|
|
if (isset($old_unreferenced_vars[$var_id])
|
2018-06-17 02:01:33 +02:00
|
|
|
|
&& $old_unreferenced_vars[$var_id] !== $locations
|
2018-01-28 23:28:34 +01:00
|
|
|
|
) {
|
2018-06-17 02:01:33 +02:00
|
|
|
|
$reassigned_vars[$var_id] = $locations;
|
2018-01-28 23:28:34 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-25 07:04:26 +01:00
|
|
|
|
}
|
2017-12-10 23:36:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
|
$try_leaves_loop = $loop_scope
|
|
|
|
|
&& $loop_scope->final_actions
|
|
|
|
|
&& !in_array(ScopeChecker::ACTION_NONE, $loop_scope->final_actions, true);
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
if (!$all_catches_leave) {
|
2018-02-27 17:23:54 +01:00
|
|
|
|
foreach ($newly_assigned_var_ids as $assigned_var_id => $_) {
|
2017-12-10 23:36:33 +01:00
|
|
|
|
$context->removeVarFromConflictingClauses($assigned_var_id);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2018-02-27 17:23:54 +01:00
|
|
|
|
foreach ($newly_assigned_var_ids as $assigned_var_id => $_) {
|
2017-12-10 23:36:33 +01:00
|
|
|
|
$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;
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
/** @var int $i */
|
|
|
|
|
foreach ($stmt->catches as $i => $catch) {
|
2016-10-22 23:35:59 +02:00
|
|
|
|
$catch_context = clone $original_context;
|
|
|
|
|
|
2016-12-04 04:41:45 +01:00
|
|
|
|
$fq_catch_classes = [];
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
|
$catch_var_name = $catch->var->name;
|
|
|
|
|
|
|
|
|
|
if (!is_string($catch_var_name)) {
|
|
|
|
|
throw new \UnexpectedValueException('Catch var name must be a string');
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-04 04:41:45 +01:00
|
|
|
|
foreach ($catch->types as $catch_type) {
|
2016-12-08 23:15:51 +01:00
|
|
|
|
$fq_catch_class = ClassLikeChecker::getFQCLNFromNameObject(
|
2016-12-04 04:41:45 +01:00
|
|
|
|
$catch_type,
|
2017-07-25 22:11:02 +02:00
|
|
|
|
$statements_checker->getAliases()
|
2016-12-04 04:41:45 +01:00
|
|
|
|
);
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
if ($original_context->check_classes) {
|
2016-12-04 04:41:45 +01:00
|
|
|
|
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
|
2018-01-02 02:04:03 +01:00
|
|
|
|
$statements_checker,
|
2016-12-04 04:41:45 +01:00
|
|
|
|
$fq_catch_class,
|
2017-06-21 20:22:52 +02:00
|
|
|
|
new CodeLocation($statements_checker->getSource(), $catch_type, $context->include_location),
|
2018-01-02 02:04:03 +01:00
|
|
|
|
$statements_checker->getSuppressedIssues(),
|
|
|
|
|
false
|
2016-12-04 04:41:45 +01:00
|
|
|
|
) === false) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2016-10-22 23:35:59 +02:00
|
|
|
|
}
|
2016-12-08 23:15:51 +01:00
|
|
|
|
|
2018-02-01 06:50:01 +01:00
|
|
|
|
if (($codebase->classExists($fq_catch_class)
|
2017-12-28 20:40:28 +01:00
|
|
|
|
&& strtolower($fq_catch_class) !== 'exception'
|
2018-02-01 06:50:01 +01:00
|
|
|
|
&& !($codebase->classExtends($fq_catch_class, 'Exception')
|
|
|
|
|
|| $codebase->classImplements($fq_catch_class, 'Throwable')))
|
|
|
|
|
|| ($codebase->interfaceExists($fq_catch_class)
|
2017-12-28 20:40:28 +01:00
|
|
|
|
&& strtolower($fq_catch_class) !== 'throwable'
|
2018-02-01 06:50:01 +01:00
|
|
|
|
&& !$codebase->interfaceExtends($fq_catch_class, 'Throwable'))
|
2017-12-28 20:40:28 +01:00
|
|
|
|
) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new InvalidCatch(
|
|
|
|
|
'Class/interface ' . $fq_catch_class . ' cannot be caught',
|
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt)
|
|
|
|
|
),
|
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-08 23:15:51 +01:00
|
|
|
|
$fq_catch_classes[] = $fq_catch_class;
|
2016-10-22 23:35:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-04-17 18:16:25 +02:00
|
|
|
|
$catch_var_id = '$' . $catch_var_name;
|
2017-02-08 00:09:12 +01:00
|
|
|
|
|
2018-02-18 23:55:11 +01:00
|
|
|
|
$catch_context->vars_in_scope[$catch_var_id] = new Union(
|
2016-12-04 04:41:45 +01:00
|
|
|
|
array_map(
|
2016-12-07 01:41:52 +01:00
|
|
|
|
/**
|
|
|
|
|
* @param string $fq_catch_class
|
2017-05-27 02:16:18 +02:00
|
|
|
|
*
|
2016-12-07 20:13:39 +01:00
|
|
|
|
* @return Type\Atomic
|
2016-12-07 01:41:52 +01:00
|
|
|
|
*/
|
2018-02-01 06:50:01 +01:00
|
|
|
|
function ($fq_catch_class) use ($codebase) {
|
2017-12-28 01:49:36 +01:00
|
|
|
|
$catch_class_type = new TNamedObject($fq_catch_class);
|
|
|
|
|
|
|
|
|
|
if (version_compare(PHP_VERSION, '7.0.0dev', '>=')
|
2018-02-01 06:50:01 +01:00
|
|
|
|
&& $codebase->interfaceExists($fq_catch_class)
|
|
|
|
|
&& !$codebase->interfaceExtends($fq_catch_class, 'Throwable')
|
2017-12-28 01:49:36 +01:00
|
|
|
|
) {
|
|
|
|
|
$catch_class_type->addIntersectionType(new TNamedObject('Throwable'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $catch_class_type;
|
2016-12-04 04:41:45 +01:00
|
|
|
|
},
|
|
|
|
|
$fq_catch_classes
|
|
|
|
|
)
|
|
|
|
|
);
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
2017-10-11 05:09:19 +02:00
|
|
|
|
// discard all clauses because crazy stuff may have happened in try block
|
|
|
|
|
$catch_context->clauses = [];
|
|
|
|
|
|
2017-02-08 00:09:12 +01:00
|
|
|
|
$catch_context->vars_possibly_in_scope[$catch_var_id] = true;
|
|
|
|
|
|
|
|
|
|
if (!$statements_checker->hasVariable($catch_var_id)) {
|
2018-01-28 23:28:34 +01:00
|
|
|
|
$location = new CodeLocation(
|
|
|
|
|
$statements_checker,
|
2018-04-17 18:16:25 +02:00
|
|
|
|
$catch->var,
|
|
|
|
|
$context->include_location
|
2018-01-28 23:28:34 +01:00
|
|
|
|
);
|
2017-02-08 00:09:12 +01:00
|
|
|
|
$statements_checker->registerVariable(
|
|
|
|
|
$catch_var_id,
|
2018-01-28 23:28:34 +01:00
|
|
|
|
$location,
|
2018-01-21 22:24:20 +01:00
|
|
|
|
$try_context->branch_point
|
2017-02-08 00:09:12 +01:00
|
|
|
|
);
|
2018-06-17 02:01:33 +02:00
|
|
|
|
$catch_context->unreferenced_vars[$catch_var_id] = [$location->getHash() => $location];
|
2017-02-08 00:09:12 +01:00
|
|
|
|
}
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
2017-02-08 06:28:26 +01:00
|
|
|
|
// this registers the variable to avoid unfair deadcode issues
|
2018-01-28 23:28:34 +01:00
|
|
|
|
$catch_context->hasVariable($catch_var_id, $statements_checker);
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
$suppressed_issues = $statements_checker->getSuppressedIssues();
|
|
|
|
|
|
|
|
|
|
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
|
|
|
|
$statements_checker->addSuppressedIssues(['RedundantCondition']);
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
|
$statements_checker->analyze($catch->stmts, $catch_context, $loop_scope);
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
|
|
|
|
$statements_checker->removeSuppressedIssues(['RedundantCondition']);
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-24 18:17:28 +01:00
|
|
|
|
$context->referenced_var_ids = array_merge(
|
|
|
|
|
$catch_context->referenced_var_ids,
|
|
|
|
|
$context->referenced_var_ids
|
|
|
|
|
);
|
2017-02-02 06:45:23 +01:00
|
|
|
|
|
2018-06-17 03:14:19 +02:00
|
|
|
|
if ($context->collect_references && $catch_actions[$i] !== [ScopeChecker::ACTION_END]) {
|
2018-01-28 23:28:34 +01:00
|
|
|
|
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
|
|
|
|
|
)
|
2018-01-25 07:04:26 +01:00
|
|
|
|
);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
|
2018-06-17 02:01:33 +02:00
|
|
|
|
foreach ($catch_context->unreferenced_vars as $var_id => $locations) {
|
2018-01-28 23:28:34 +01:00
|
|
|
|
if (!isset($old_unreferenced_vars[$var_id])
|
2018-02-27 17:23:54 +01:00
|
|
|
|
&& (isset($context->unreferenced_vars[$var_id])
|
|
|
|
|
|| isset($newly_assigned_var_ids[$var_id]))
|
2018-01-28 23:28:34 +01:00
|
|
|
|
) {
|
2018-06-17 02:01:33 +02:00
|
|
|
|
$statements_checker->registerVariableUses($locations);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
} elseif (isset($old_unreferenced_vars[$var_id])
|
2018-06-17 02:01:33 +02:00
|
|
|
|
&& $old_unreferenced_vars[$var_id] !== $locations
|
2018-01-28 23:28:34 +01:00
|
|
|
|
) {
|
2018-06-17 02:01:33 +02:00
|
|
|
|
$statements_checker->registerVariableUses($locations);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-25 07:04:26 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
if ($catch_actions[$i] !== [ScopeChecker::ACTION_END]) {
|
2017-12-11 00:09:38 +01:00
|
|
|
|
foreach ($catch_context->vars_in_scope as $var_id => $type) {
|
2018-04-18 18:10:49 +02:00
|
|
|
|
if ($context->hasVariable($var_id)
|
2018-04-17 21:39:09 +02:00
|
|
|
|
&& $context->vars_in_scope[$var_id]->getId() !== $type->getId()
|
2016-11-02 07:29:00 +01:00
|
|
|
|
) {
|
2017-12-11 00:09:38 +01:00
|
|
|
|
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
|
|
|
|
$context->vars_in_scope[$var_id],
|
2016-11-02 07:29:00 +01:00
|
|
|
|
$type
|
|
|
|
|
);
|
2016-10-22 23:35:59 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
|
$context->vars_possibly_in_scope = array_merge(
|
|
|
|
|
$catch_context->vars_possibly_in_scope,
|
|
|
|
|
$context->vars_possibly_in_scope
|
|
|
|
|
);
|
2016-10-22 23:35:59 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
|
if ($loop_scope
|
|
|
|
|
&& !$try_leaves_loop
|
|
|
|
|
&& !in_array(ScopeChecker::ACTION_NONE, $loop_scope->final_actions, true)
|
|
|
|
|
) {
|
|
|
|
|
$loop_scope->final_actions[] = ScopeChecker::ACTION_NONE;
|
2016-10-22 23:35:59 +02:00
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
2017-12-10 23:36:33 +01:00
|
|
|
|
if ($stmt->finally) {
|
|
|
|
|
$statements_checker->analyze($stmt->finally->stmts, $context, $loop_scope);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
|
if ($context->collect_references) {
|
2018-06-17 02:01:33 +02:00
|
|
|
|
foreach ($old_unreferenced_vars as $var_id => $locations) {
|
|
|
|
|
if (isset($context->unreferenced_vars[$var_id])
|
|
|
|
|
&& $context->unreferenced_vars[$var_id] !== $locations
|
|
|
|
|
) {
|
|
|
|
|
$statements_checker->registerVariableUses($locations);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
|
return null;
|
2016-10-22 23:35:59 +02:00
|
|
|
|
}
|
|
|
|
|
}
|