1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-16 03:17:02 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php

375 lines
14 KiB
PHP
Raw Normal View History

2016-10-22 23:35:59 +02:00
<?php
2018-11-06 03:57:36 +01:00
namespace Psalm\Internal\Analyzer\Statements\Block;
2016-10-22 23:35:59 +02:00
use PhpParser;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\ScopeAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\CodeLocation;
2016-11-02 07:29:00 +01:00
use Psalm\Context;
use Psalm\Issue\InvalidCatch;
use Psalm\IssueBuffer;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Scope\LoopScope;
2016-10-22 23:35:59 +02:00
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
2016-10-22 23:35:59 +02:00
2018-11-06 03:57:36 +01:00
class TryAnalyzer
2016-10-22 23:35:59 +02:00
{
/**
2018-11-06 03:57:36 +01:00
* @param StatementsAnalyzer $statements_checker
2016-11-02 07:29:00 +01:00
* @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
*/
public static function analyze(
2018-11-06 03:57:36 +01:00
StatementsAnalyzer $statements_checker,
2016-11-02 07:29:00 +01:00
PhpParser\Node\Stmt\TryCatch $stmt,
Context $context
2016-11-02 07:29:00 +01:00
) {
$catch_actions = [];
$all_catches_leave = true;
2018-11-06 03:57:36 +01:00
$codebase = $statements_checker->getCodebase();
/** @var int $i */
foreach ($stmt->catches as $i => $catch) {
2018-11-06 03:57:36 +01:00
$catch_actions[$i] = ScopeAnalyzer::getFinalControlActions(
$catch->stmts,
$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, bool>
*/
$context->possibly_thrown_exceptions = [];
if ($all_catches_leave) {
$try_context = $context;
} else {
$try_context = clone $context;
2018-11-06 03:57:36 +01:00
if ($codebase->alter_code) {
$try_context->branch_point = $try_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
}
}
$assigned_var_ids = $try_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) === false) {
return false;
}
/** @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
);
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])) {
$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;
$type->possibly_undefined_from_try = true;
} 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],
$type
);
}
}
2017-12-11 00:34:22 +01:00
$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
)
);
2018-06-17 02:01:33 +02:00
foreach ($context->unreferenced_vars as $var_id => $locations) {
if (isset($old_unreferenced_vars[$var_id])
2018-06-17 02:01:33 +02:00
&& $old_unreferenced_vars[$var_id] !== $locations
) {
2018-06-17 02:01:33 +02:00
$reassigned_vars[$var_id] = $locations;
}
}
}
}
$try_leaves_loop = $context->loop_scope
&& $context->loop_scope->final_actions
2018-11-06 03:57:36 +01:00
&& !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true);
2016-10-22 23:35:59 +02:00
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;
2016-10-22 23:35:59 +02:00
/** @var int $i */
foreach ($stmt->catches as $i => $catch) {
2016-10-22 23:35:59 +02:00
$catch_context = clone $original_context;
$fq_catch_classes = [];
2016-10-22 23:35:59 +02:00
$catch_var_name = $catch->var->name;
if (!is_string($catch_var_name)) {
throw new \UnexpectedValueException('Catch var name must be a string');
}
foreach ($catch->types as $catch_type) {
2018-11-06 03:57:36 +01:00
$fq_catch_class = ClassLikeAnalyzer::getFQCLNFromNameObject(
$catch_type,
Refactor scanning and analysis, introducing multithreading (#191) * Add failing test * Add visitor to soup up classlike references * Move a whole bunch of code into the visitor * Move some methods back, move onto analysis stage * Use the getAliases method everywhere * Fix refs * Fix more refs * Fix some tests * Fix more tests * Fix include tests * Shift config class finding to project checker and fix bugs * Fix a few more tests * transition test to new syntax * Remove var_dump * Delete a bunch of code and fix mutation test * Remove unnecessary visitation * Transition to better mocked out file provider, breaking some cached statement loading * Use different scheme for naming anonymous classes * Fix anonymous class issues * Refactor file/statement loading * Add specific property types * Fix mapped property assignment * Improve how we deal with traits * Fix trait checking * Pass Psalm checks * Add multi-process support * Delay console output until the end * Remove PHP 7 syntax * Update file storage with classes * Fix scanning individual files and add reflection return types * Always turn XDebug off * Add quicker method of getting method mutations * Queue return types for crawling * Interpret all strings as possible classes once we see a `get_class` call * Check invalid return types again * Fix template namespacing issues * Default to class-insensitive file names for includes * Don’t overwrite existing issues data * Add var docblocks for scanning * Add null check * Fix loading of external classes in templates * Only try to populate class when we haven’t yet seen it’s not a class * Fix trait property accessibility * Only ever improve docblock param type * Make param replacement more robust * Fix static const missing inferred type * Fix a few more tests * Register constant definitions * Fix trait aliasing * Skip constant type tests for now * Fix linting issues * Make sure caching is off for tests * Remove unnecessary return * Use emulative parser if on PHP 5.6 * Cache parser for faster first-time parse * Fix constant resolution when scanning classes * Remove test that’s beyond a practical scope * Add back --diff support * Add --help for --threads * Remove unused vars
2017-07-25 22:11:02 +02:00
$statements_checker->getAliases()
);
2016-10-22 23:35:59 +02:00
if ($original_context->check_classes) {
2018-11-06 03:57:36 +01:00
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
$statements_checker,
$fq_catch_class,
new CodeLocation($statements_checker->getSource(), $catch_type, $context->include_location),
$statements_checker->getSuppressedIssues(),
false
) === 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)
&& 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)
&& strtolower($fq_catch_class) !== 'throwable'
2018-02-01 06:50:01 +01:00
&& !$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),
$fq_catch_class
),
$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
}
$potentially_caught_classes = array_flip($fq_catch_classes);
if ($catch_context->collect_exceptions) {
foreach ($fq_catch_classes as $fq_catch_class) {
$fq_catch_class_lower = strtolower($fq_catch_class);
foreach ($context->possibly_thrown_exceptions as $exception_fqcln => $_) {
$exception_fqcln_lower = strtolower($exception_fqcln);
if ($exception_fqcln_lower === $fq_catch_class_lower) {
unset($context->possibly_thrown_exceptions[$exception_fqcln]);
continue;
}
if ($codebase->classExists($exception_fqcln)
&& $codebase->classExtendsOrImplements(
$exception_fqcln,
$fq_catch_class
)
) {
unset($context->possibly_thrown_exceptions[$exception_fqcln]);
continue;
}
if ($codebase->interfaceExists($exception_fqcln)
&& $codebase->interfaceExtends(
$exception_fqcln,
$fq_catch_class
)
) {
unset($context->possibly_thrown_exceptions[$exception_fqcln]);
continue;
}
}
}
}
$catch_var_id = '$' . $catch_var_name;
$catch_context->vars_in_scope[$catch_var_id] = new Union(
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) {
$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')
) {
$catch_class_type->addIntersectionType(new TNamedObject('Throwable'));
}
return $catch_class_type;
},
$fq_catch_classes
)
);
2016-10-22 23:35:59 +02:00
// 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->var,
$context->include_location
);
$statements_checker->registerVariable(
$catch_var_id,
$location,
$try_context->branch_point
);
2018-06-17 02:01:33 +02:00
$catch_context->unreferenced_vars[$catch_var_id] = [$location->getHash() => $location];
}
2016-10-22 23:35:59 +02:00
// this registers the variable to avoid unfair deadcode issues
$catch_context->hasVariable($catch_var_id, $statements_checker);
2016-10-22 23:35:59 +02:00
$suppressed_issues = $statements_checker->getSuppressedIssues();
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
$statements_checker->addSuppressedIssues(['RedundantCondition']);
}
$statements_checker->analyze($catch->stmts, $catch_context);
2016-10-22 23:35:59 +02:00
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
);
2017-02-02 06:45:23 +01:00
2018-11-06 03:57:36 +01:00
if ($context->collect_references && $catch_actions[$i] !== [ScopeAnalyzer::ACTION_END]) {
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-06-17 02:01:33 +02:00
foreach ($catch_context->unreferenced_vars as $var_id => $locations) {
if (!isset($old_unreferenced_vars[$var_id])
&& (isset($context->unreferenced_vars[$var_id])
|| isset($newly_assigned_var_ids[$var_id]))
) {
2018-06-17 02:01:33 +02:00
$statements_checker->registerVariableUses($locations);
} elseif (isset($old_unreferenced_vars[$var_id])
2018-06-17 02:01:33 +02:00
&& $old_unreferenced_vars[$var_id] !== $locations
) {
2018-06-17 02:01:33 +02:00
$statements_checker->registerVariableUses($locations);
}
}
}
if ($context->collect_exceptions) {
$potentially_caught_classes = array_diff_key(
$potentially_caught_classes,
$context->possibly_thrown_exceptions
);
}
2018-11-06 03:57:36 +01:00
if ($catch_actions[$i] !== [ScopeAnalyzer::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)
&& $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
}
}
if ($context->loop_scope
2017-12-03 00:28:18 +01:00
&& !$try_leaves_loop
2018-11-06 03:57:36 +01:00
&& !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true)
2017-12-03 00:28:18 +01:00
) {
2018-11-06 03:57:36 +01:00
$context->loop_scope->final_actions[] = ScopeAnalyzer::ACTION_NONE;
2016-10-22 23:35:59 +02:00
}
2016-11-02 07:29:00 +01:00
if ($stmt->finally) {
$statements_checker->analyze($stmt->finally->stmts, $context);
}
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);
}
}
}
$context->possibly_thrown_exceptions += $existing_thrown_exceptions;
2016-11-02 07:29:00 +01:00
return null;
2016-10-22 23:35:59 +02:00
}
}