1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-15 10:57:08 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php
2021-06-07 22:55:21 -04:00

525 lines
21 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\Internal\Analyzer\Statements\Block;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\ClassLikeNameOptions;
use Psalm\Internal\Analyzer\ScopeAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\Scope\FinallyScope;
use Psalm\Issue\InvalidCatch;
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
use function array_intersect_key;
use function array_map;
use function array_merge;
use function in_array;
use function is_string;
use function strtolower;
use function version_compare;
use const PHP_VERSION;
/**
* @internal
*/
class TryAnalyzer
{
/**
* @return false|null
*/
public static function analyze(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Stmt\TryCatch $stmt,
Context $context
): ?bool {
$catch_actions = [];
$all_catches_leave = true;
$codebase = $statements_analyzer->getCodebase();
/** @var int $i */
foreach ($stmt->catches as $i => $catch) {
$catch_actions[$i] = ScopeAnalyzer::getControlActions(
$catch->stmts,
$statements_analyzer->node_data,
$codebase->config->exit_functions,
[]
);
$all_catches_leave = $all_catches_leave && !in_array(ScopeAnalyzer::ACTION_NONE, $catch_actions[$i], true);
}
$existing_thrown_exceptions = $context->possibly_thrown_exceptions;
/**
* @var array<string, array<array-key, CodeLocation>>
*/
$context->possibly_thrown_exceptions = [];
$old_context = clone $context;
if ($all_catches_leave && !$stmt->finally) {
$try_context = $context;
} else {
$try_context = clone $context;
if ($codebase->alter_code) {
$try_context->branch_point = $try_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
}
if ($stmt->finally) {
$try_context->finally_scope = new FinallyScope($try_context->vars_in_scope);
}
}
$assigned_var_ids = $try_context->assigned_var_ids;
$context->assigned_var_ids = [];
$old_referenced_var_ids = $try_context->referenced_var_ids;
if ($statements_analyzer->analyze($stmt->stmts, $context) === false) {
return false;
}
if ($try_context->finally_scope) {
foreach ($context->vars_in_scope as $var_id => $type) {
if (isset($try_context->finally_scope->vars_in_scope[$var_id])) {
if ($try_context->finally_scope->vars_in_scope[$var_id] !== $type) {
$try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes(
$try_context->finally_scope->vars_in_scope[$var_id],
$type,
$statements_analyzer->getCodebase()
);
}
} else {
$try_context->finally_scope->vars_in_scope[$var_id] = $type;
}
}
}
$context->has_returned = false;
$try_block_control_actions = ScopeAnalyzer::getControlActions(
$stmt->stmts,
$statements_analyzer->node_data,
$codebase->config->exit_functions,
[]
);
/** @var array<string, int> */
$newly_assigned_var_ids = $context->assigned_var_ids;
$context->assigned_var_ids = array_merge(
$assigned_var_ids,
$newly_assigned_var_ids
);
$possibly_referenced_var_ids = array_merge(
$context->referenced_var_ids,
$old_referenced_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;
$context->vars_in_scope[$var_id]->possibly_undefined = true;
$context->vars_in_scope[$var_id]->possibly_undefined_from_try = 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;
$try_context->possibly_thrown_exceptions = $context->possibly_thrown_exceptions;
$context->referenced_var_ids = array_intersect_key(
$try_context->referenced_var_ids,
$context->referenced_var_ids
);
}
$try_leaves_loop = $context->loop_scope
&& $context->loop_scope->final_actions
&& !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true);
if (!$all_catches_leave) {
foreach ($newly_assigned_var_ids as $assigned_var_id => $_) {
$context->removeVarFromConflictingClauses($assigned_var_id);
}
} else {
foreach ($newly_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;
$issues_to_suppress = [
'RedundantCondition',
'RedundantConditionGivenDocblockType',
'TypeDoesNotContainNull',
'TypeDoesNotContainType',
];
$definitely_newly_assigned_var_ids = $newly_assigned_var_ids;
/** @var int $i */
foreach ($stmt->catches as $i => $catch) {
$catch_context = clone $original_context;
$catch_context->has_returned = false;
foreach ($catch_context->vars_in_scope as $var_id => $type) {
if (!isset($old_context->vars_in_scope[$var_id])) {
$type = clone $type;
$type->possibly_undefined_from_try = true;
$catch_context->vars_in_scope[$var_id] = $type;
} else {
$catch_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
$type,
$old_context->vars_in_scope[$var_id]
);
}
}
$fq_catch_classes = [];
if (!$catch->types) {
throw new \UnexpectedValueException('Very bad');
}
foreach ($catch->types as $catch_type) {
$fq_catch_class = ClassLikeAnalyzer::getFQCLNFromNameObject(
$catch_type,
$statements_analyzer->getAliases()
);
$fq_catch_class = $codebase->classlikes->getUnAliasedName($fq_catch_class);
if ($codebase->alter_code && $fq_catch_class) {
$codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$catch_type,
$fq_catch_class,
$context->calling_method_id
);
}
if ($original_context->check_classes) {
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_analyzer,
$fq_catch_class,
new CodeLocation($statements_analyzer->getSource(), $catch_type, $context->include_location),
$context->self,
$context->calling_method_id,
$statements_analyzer->getSuppressedIssues(),
new ClassLikeNameOptions(true)
) === false) {
// fall through
}
}
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_analyzer->getSource(), $stmt),
$fq_catch_class
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
$fq_catch_classes[] = $fq_catch_class;
}
if ($catch_context->collect_exceptions) {
foreach ($fq_catch_classes as $fq_catch_class) {
$fq_catch_class_lower = strtolower($fq_catch_class);
foreach ($catch_context->possibly_thrown_exceptions as $exception_fqcln => $_) {
$exception_fqcln_lower = strtolower($exception_fqcln);
if ($exception_fqcln_lower === $fq_catch_class_lower
|| ($codebase->classExists($exception_fqcln)
&& $codebase->classExtendsOrImplements($exception_fqcln, $fq_catch_class))
|| ($codebase->interfaceExists($exception_fqcln)
&& $codebase->interfaceExtends($exception_fqcln, $fq_catch_class))
) {
unset($original_context->possibly_thrown_exceptions[$exception_fqcln]);
unset($context->possibly_thrown_exceptions[$exception_fqcln]);
unset($catch_context->possibly_thrown_exceptions[$exception_fqcln]);
}
}
}
/**
* @var array<string, array<array-key, CodeLocation>>
*/
$catch_context->possibly_thrown_exceptions = [];
}
// discard all clauses because crazy stuff may have happened in try block
$catch_context->clauses = [];
if ($catch->var && is_string($catch->var->name)) {
$catch_var_id = '$' . $catch->var->name;
$catch_context->vars_in_scope[$catch_var_id] = new Union(
array_map(
/**
* @param string $fq_catch_class
*/
function ($fq_catch_class) use ($codebase): \Psalm\Type\Atomic\TNamedObject {
$catch_class_type = new TNamedObject($fq_catch_class);
if (version_compare(PHP_VERSION, '7.0.0dev', '>=')
&& strtolower($fq_catch_class) !== 'throwable'
&& $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
)
);
// removes dependent vars from $context
$catch_context->removeDescendents(
$catch_var_id,
null,
$catch_context->vars_in_scope[$catch_var_id],
$statements_analyzer
);
$catch_context->vars_possibly_in_scope[$catch_var_id] = true;
$location = new CodeLocation($statements_analyzer->getSource(), $catch->var);
if (!$statements_analyzer->hasVariable($catch_var_id)) {
$statements_analyzer->registerVariable(
$catch_var_id,
$location,
$catch_context->branch_point
);
} else {
$statements_analyzer->registerVariableAssignment(
$catch_var_id,
$location
);
}
if ($statements_analyzer->data_flow_graph) {
$catch_var_node = DataFlowNode::getForAssignment($catch_var_id, $location);
$catch_context->vars_in_scope[$catch_var_id]->parent_nodes = [
$catch_var_node->id => $catch_var_node
];
if ($statements_analyzer->data_flow_graph instanceof \Psalm\Internal\Codebase\VariableUseGraph) {
$statements_analyzer->data_flow_graph->addPath(
$catch_var_node,
new DataFlowNode('variable-use', 'variable use', null),
'variable-use'
);
}
}
}
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
foreach ($issues_to_suppress as $issue_to_suppress) {
if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues([$issue_to_suppress]);
}
}
$old_catch_assigned_var_ids = $catch_context->referenced_var_ids;
$catch_context->assigned_var_ids = [];
$statements_analyzer->analyze($catch->stmts, $catch_context);
// recalculate in case there's a no-return clause
$catch_actions[$i] = ScopeAnalyzer::getControlActions(
$catch->stmts,
$statements_analyzer->node_data,
$codebase->config->exit_functions,
[]
);
foreach ($issues_to_suppress as $issue_to_suppress) {
if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues([$issue_to_suppress]);
}
}
/** @var array<string, bool> */
$new_catch_assigned_var_ids = $catch_context->assigned_var_ids;
$catch_context->assigned_var_ids += $old_catch_assigned_var_ids;
$context->referenced_var_ids = array_intersect_key(
$catch_context->referenced_var_ids,
$context->referenced_var_ids
);
$possibly_referenced_var_ids = array_merge(
$catch_context->referenced_var_ids,
$possibly_referenced_var_ids
);
if ($catch_context->collect_exceptions) {
$context->mergeExceptions($catch_context);
}
$catch_doesnt_leave_parent_scope = $catch_actions[$i] !== [ScopeAnalyzer::ACTION_END]
&& $catch_actions[$i] !== [ScopeAnalyzer::ACTION_CONTINUE]
&& $catch_actions[$i] !== [ScopeAnalyzer::ACTION_BREAK];
if ($catch_doesnt_leave_parent_scope) {
$definitely_newly_assigned_var_ids = array_intersect_key(
$new_catch_assigned_var_ids,
$definitely_newly_assigned_var_ids
);
foreach ($catch_context->vars_in_scope as $var_id => $type) {
if ($try_block_control_actions === [ScopeAnalyzer::ACTION_END]) {
$context->vars_in_scope[$var_id] = $type;
} elseif (isset($context->vars_in_scope[$var_id])) {
$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
);
} else {
if ($stmt->finally) {
$context->vars_possibly_in_scope = array_merge(
$catch_context->vars_possibly_in_scope,
$context->vars_possibly_in_scope
);
}
}
if ($try_context->finally_scope) {
foreach ($catch_context->vars_in_scope as $var_id => $type) {
if (isset($try_context->finally_scope->vars_in_scope[$var_id])) {
if ($try_context->finally_scope->vars_in_scope[$var_id] !== $type) {
$try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes(
$try_context->finally_scope->vars_in_scope[$var_id],
$type,
$statements_analyzer->getCodebase()
);
}
} else {
$try_context->finally_scope->vars_in_scope[$var_id] = $type;
$type->possibly_undefined = true;
$type->possibly_undefined_from_try = true;
}
}
}
}
if ($context->loop_scope
&& !$try_leaves_loop
&& !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true)
) {
$context->loop_scope->final_actions[] = ScopeAnalyzer::ACTION_NONE;
}
$finally_has_returned = false;
if ($stmt->finally) {
if ($try_context->finally_scope) {
$finally_context = clone $context;
$finally_context->assigned_var_ids = [];
$finally_context->possibly_assigned_var_ids = [];
$finally_context->vars_in_scope = $try_context->finally_scope->vars_in_scope;
$statements_analyzer->analyze($stmt->finally->stmts, $finally_context);
$finally_has_returned = $finally_context->has_returned;
/** @var string $var_id */
foreach ($finally_context->assigned_var_ids as $var_id => $_) {
if (isset($context->vars_in_scope[$var_id])
&& isset($finally_context->vars_in_scope[$var_id])
) {
if ($context->vars_in_scope[$var_id]->possibly_undefined
&& $context->vars_in_scope[$var_id]->possibly_undefined_from_try
) {
$context->vars_in_scope[$var_id]->possibly_undefined = false;
$context->vars_in_scope[$var_id]->possibly_undefined_from_try = false;
}
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
$context->vars_in_scope[$var_id],
$finally_context->vars_in_scope[$var_id],
$codebase
);
} elseif (isset($finally_context->vars_in_scope[$var_id])) {
$context->vars_in_scope[$var_id] = clone $finally_context->vars_in_scope[$var_id];
}
}
}
}
foreach ($definitely_newly_assigned_var_ids as $var_id => $_) {
if (isset($context->vars_in_scope[$var_id])) {
$new_type = clone $context->vars_in_scope[$var_id];
if ($new_type->possibly_undefined_from_try) {
$new_type->possibly_undefined = false;
$new_type->possibly_undefined_from_try = false;
}
$context->vars_in_scope[$var_id] = $new_type;
}
}
foreach ($existing_thrown_exceptions as $possibly_thrown_exception => $codelocations) {
foreach ($codelocations as $hash => $codelocation) {
$context->possibly_thrown_exceptions[$possibly_thrown_exception][$hash] = $codelocation;
}
}
$body_has_returned = !in_array(ScopeAnalyzer::ACTION_NONE, $try_block_control_actions, true);
$context->has_returned = ($body_has_returned && $all_catches_leave) || $finally_has_returned;
return null;
}
}