2016-05-09 14:56:07 +02:00
|
|
|
<?php
|
2018-11-06 03:57:36 +01:00
|
|
|
namespace Psalm\Internal\Analyzer;
|
2016-05-09 14:56:07 +02:00
|
|
|
|
|
|
|
use PhpParser;
|
|
|
|
|
2018-12-02 00:37:49 +01:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
class ScopeAnalyzer
|
2016-05-09 14:56:07 +02:00
|
|
|
{
|
2017-11-28 06:25:21 +01:00
|
|
|
const ACTION_END = 'END';
|
|
|
|
const ACTION_BREAK = 'BREAK';
|
|
|
|
const ACTION_CONTINUE = 'CONTINUE';
|
2018-06-17 02:01:33 +02:00
|
|
|
const ACTION_LEAVE_SWITCH = 'LEAVE_SWITCH';
|
2017-11-28 06:25:21 +01:00
|
|
|
const ACTION_NONE = 'NONE';
|
2018-02-23 21:39:33 +01:00
|
|
|
const ACTION_RETURN = 'RETURN';
|
2017-11-28 06:25:21 +01:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2018-04-17 18:16:25 +02:00
|
|
|
* @param array<PhpParser\Node\Stmt> $stmts
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-21 22:52:27 +01:00
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public static function doesEverBreak(array $stmts)
|
|
|
|
{
|
|
|
|
if (empty($stmts)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-05-27 02:05:57 +02:00
|
|
|
for ($i = count($stmts) - 1; $i >= 0; --$i) {
|
2017-02-21 22:52:27 +01:00
|
|
|
$stmt = $stmts[$i];
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Break_) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
|
|
|
if (self::doesEverBreak($stmt->stmts)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt->else && self::doesEverBreak($stmt->else->stmts)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($stmt->elseifs as $elseif) {
|
|
|
|
if (self::doesEverBreak($elseif->stmts)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2016-12-17 06:48:31 +01:00
|
|
|
* @param array<PhpParser\Node> $stmts
|
2018-06-17 02:01:33 +02:00
|
|
|
* @param bool $in_switch when checking inside a switch statement, continue is an alias of break
|
2018-02-23 21:39:33 +01:00
|
|
|
* @param bool $return_is_exit Exit and Throw statements are treated differently from return if this is false
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-11-28 06:25:21 +01:00
|
|
|
* @return string[] one or more of 'LEAVE', 'CONTINUE', 'BREAK' (or empty if no single action is found)
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2018-02-23 21:39:33 +01:00
|
|
|
public static function getFinalControlActions(
|
|
|
|
array $stmts,
|
2018-07-13 05:26:08 +02:00
|
|
|
array $exit_functions,
|
2018-06-17 02:01:33 +02:00
|
|
|
$in_switch = false,
|
2018-02-23 21:39:33 +01:00
|
|
|
$return_is_exit = true
|
|
|
|
) {
|
2016-06-20 22:18:31 +02:00
|
|
|
if (empty($stmts)) {
|
2018-01-03 03:23:48 +01:00
|
|
|
return [self::ACTION_NONE];
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
$control_actions = [];
|
|
|
|
|
|
|
|
for ($i = 0, $c = count($stmts); $i < $c; ++$i) {
|
2016-06-20 22:18:31 +02:00
|
|
|
$stmt = $stmts[$i];
|
|
|
|
|
2016-06-21 00:10:24 +02:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Return_ ||
|
|
|
|
$stmt instanceof PhpParser\Node\Stmt\Throw_ ||
|
2018-04-17 18:16:25 +02:00
|
|
|
($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Exit_)
|
2016-06-21 00:10:24 +02:00
|
|
|
) {
|
2018-02-23 21:39:33 +01:00
|
|
|
if (!$return_is_exit && $stmt instanceof PhpParser\Node\Stmt\Return_) {
|
|
|
|
return [self::ACTION_RETURN];
|
|
|
|
}
|
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
return [self::ACTION_END];
|
|
|
|
}
|
|
|
|
|
2018-07-13 05:26:08 +02:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
|
2018-07-14 00:36:25 +02:00
|
|
|
if ($stmt->expr instanceof PhpParser\Node\Expr\FuncCall
|
|
|
|
&& $stmt->expr->name instanceof PhpParser\Node\Name
|
|
|
|
&& $stmt->expr->name->parts === ['trigger_error']
|
|
|
|
&& isset($stmt->expr->args[1])
|
|
|
|
&& $stmt->expr->args[1]->value instanceof PhpParser\Node\Expr\ConstFetch
|
|
|
|
&& in_array(
|
|
|
|
end($stmt->expr->args[1]->value->name->parts),
|
|
|
|
['E_ERROR', 'E_PARSE', 'E_CORE_ERROR', 'E_COMPILE_ERROR', 'E_USER_ERROR']
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
return [self::ACTION_END];
|
|
|
|
}
|
|
|
|
|
2019-01-02 23:16:04 +01:00
|
|
|
// This allows calls to functions that always exit to act as exit statements themselves
|
|
|
|
if (!$return_is_exit
|
|
|
|
&& isset($stmt->expr->inferredType)
|
|
|
|
&& $stmt->expr->inferredType->isNever()
|
|
|
|
) {
|
|
|
|
return [self::ACTION_END];
|
|
|
|
}
|
|
|
|
|
2018-07-13 05:26:08 +02:00
|
|
|
if ($exit_functions) {
|
|
|
|
if ($stmt->expr instanceof PhpParser\Node\Expr\FuncCall
|
|
|
|
|| $stmt->expr instanceof PhpParser\Node\Expr\StaticCall
|
|
|
|
) {
|
|
|
|
if ($stmt->expr instanceof PhpParser\Node\Expr\FuncCall) {
|
|
|
|
/** @var string|null */
|
|
|
|
$resolved_name = $stmt->expr->name->getAttribute('resolvedName');
|
|
|
|
|
|
|
|
if ($resolved_name && isset($exit_functions[strtolower($resolved_name)])) {
|
|
|
|
return [self::ACTION_END];
|
|
|
|
}
|
|
|
|
} elseif ($stmt->expr->class instanceof PhpParser\Node\Name
|
|
|
|
&& $stmt->expr->name instanceof PhpParser\Node\Identifier
|
|
|
|
) {
|
|
|
|
/** @var string|null */
|
|
|
|
$resolved_class_name = $stmt->expr->class->getAttribute('resolvedName');
|
|
|
|
|
|
|
|
if ($resolved_class_name
|
|
|
|
&& isset($exit_functions[strtolower($resolved_class_name . '::' . $stmt->expr->name)])
|
|
|
|
) {
|
|
|
|
return [self::ACTION_END];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
|
2018-06-17 02:01:33 +02:00
|
|
|
if ($in_switch
|
2018-01-24 06:01:08 +01:00
|
|
|
&& (!$stmt->num || !$stmt->num instanceof PhpParser\Node\Scalar\LNumber || $stmt->num->value < 2)
|
|
|
|
) {
|
2018-06-17 02:01:33 +02:00
|
|
|
return [self::ACTION_LEAVE_SWITCH];
|
2018-01-24 06:01:08 +01:00
|
|
|
}
|
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
return [self::ACTION_CONTINUE];
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Break_) {
|
2018-06-17 02:01:33 +02:00
|
|
|
if ($in_switch
|
|
|
|
&& (!$stmt->num || !$stmt->num instanceof PhpParser\Node\Scalar\LNumber || $stmt->num->value < 2)
|
|
|
|
) {
|
|
|
|
return [self::ACTION_LEAVE_SWITCH];
|
|
|
|
}
|
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
return [self::ACTION_BREAK];
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
2018-07-13 05:26:08 +02:00
|
|
|
$if_statement_actions = self::getFinalControlActions($stmt->stmts, $exit_functions, $in_switch);
|
2018-01-24 06:01:08 +01:00
|
|
|
$else_statement_actions = $stmt->else
|
2018-07-13 05:26:08 +02:00
|
|
|
? self::getFinalControlActions($stmt->else->stmts, $exit_functions, $in_switch)
|
2018-01-24 06:01:08 +01:00
|
|
|
: [];
|
2016-06-20 22:18:31 +02:00
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
$all_same = count($if_statement_actions) === 1
|
|
|
|
&& $if_statement_actions == $else_statement_actions
|
|
|
|
&& $if_statement_actions !== [self::ACTION_NONE];
|
|
|
|
|
|
|
|
$all_elseif_actions = [];
|
|
|
|
|
|
|
|
if ($stmt->elseifs) {
|
2016-06-20 22:18:31 +02:00
|
|
|
foreach ($stmt->elseifs as $elseif) {
|
2018-07-13 05:26:08 +02:00
|
|
|
$elseif_control_actions = self::getFinalControlActions(
|
|
|
|
$elseif->stmts,
|
|
|
|
$exit_functions,
|
|
|
|
$in_switch
|
|
|
|
);
|
2017-11-28 06:25:21 +01:00
|
|
|
|
|
|
|
$all_same = $all_same && $elseif_control_actions == $if_statement_actions;
|
|
|
|
|
|
|
|
if (!$all_same) {
|
|
|
|
$all_elseif_actions = array_merge($elseif_control_actions, $all_elseif_actions);
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
}
|
2017-11-28 06:25:21 +01:00
|
|
|
}
|
2016-06-20 22:18:31 +02:00
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
if ($all_same) {
|
|
|
|
return $if_statement_actions;
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
2017-11-28 06:25:21 +01:00
|
|
|
|
|
|
|
$control_actions = array_merge(
|
|
|
|
$control_actions,
|
|
|
|
$if_statement_actions,
|
|
|
|
$else_statement_actions,
|
|
|
|
$all_elseif_actions
|
|
|
|
);
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
2016-06-28 19:56:44 +02:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
|
2018-02-23 21:39:33 +01:00
|
|
|
$has_ended = false;
|
2018-01-24 22:15:53 +01:00
|
|
|
$has_non_breaking_default = false;
|
2016-06-28 20:28:45 +02:00
|
|
|
$has_default_terminator = false;
|
2016-06-28 19:56:44 +02:00
|
|
|
|
|
|
|
// iterate backwards in a case statement
|
2017-11-28 06:25:21 +01:00
|
|
|
for ($d = count($stmt->cases) - 1; $d >= 0; --$d) {
|
|
|
|
$case = $stmt->cases[$d];
|
2016-06-20 22:18:31 +02:00
|
|
|
|
2018-07-13 05:26:08 +02:00
|
|
|
$case_actions = self::getFinalControlActions($case->stmts, $exit_functions, true);
|
2017-11-28 06:25:21 +01:00
|
|
|
|
2018-06-17 02:01:33 +02:00
|
|
|
if (array_intersect([
|
|
|
|
self::ACTION_LEAVE_SWITCH,
|
|
|
|
self::ACTION_BREAK,
|
|
|
|
self::ACTION_CONTINUE
|
|
|
|
], $case_actions)
|
|
|
|
) {
|
2017-11-28 06:25:21 +01:00
|
|
|
continue 2;
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
2018-01-24 22:15:53 +01:00
|
|
|
if (!$case->cond) {
|
|
|
|
$has_non_breaking_default = true;
|
|
|
|
}
|
|
|
|
|
2018-02-23 21:39:33 +01:00
|
|
|
$case_does_end = $case_actions == [self::ACTION_END];
|
2016-06-28 19:56:44 +02:00
|
|
|
|
2018-02-23 21:39:33 +01:00
|
|
|
if ($case_does_end) {
|
|
|
|
$has_ended = true;
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
2018-02-23 21:39:33 +01:00
|
|
|
if (!$case_does_end && !$has_ended) {
|
2017-11-28 06:25:21 +01:00
|
|
|
continue 2;
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
2018-02-23 21:39:33 +01:00
|
|
|
if ($has_non_breaking_default && $case_does_end) {
|
2016-06-28 19:56:44 +02:00
|
|
|
$has_default_terminator = true;
|
|
|
|
}
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
2018-05-24 20:26:29 +02:00
|
|
|
if ($has_default_terminator || isset($stmt->allMatched)) {
|
2017-11-28 06:25:21 +01:00
|
|
|
return [self::ACTION_END];
|
|
|
|
}
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
2016-06-21 00:10:24 +02:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\While_) {
|
2018-07-13 05:26:08 +02:00
|
|
|
$control_actions = array_merge(
|
|
|
|
self::getFinalControlActions($stmt->stmts, $exit_functions),
|
|
|
|
$control_actions
|
|
|
|
);
|
2017-11-28 06:25:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Do_) {
|
2018-07-13 05:26:08 +02:00
|
|
|
$do_actions = self::getFinalControlActions($stmt->stmts, $exit_functions);
|
2017-11-28 06:25:21 +01:00
|
|
|
|
|
|
|
if (count($do_actions) && !in_array(self::ACTION_NONE, $do_actions, true)) {
|
|
|
|
return $do_actions;
|
2016-06-21 00:10:24 +02:00
|
|
|
}
|
2017-11-28 06:25:21 +01:00
|
|
|
|
|
|
|
$control_actions = array_merge($control_actions, $do_actions);
|
2016-06-21 00:10:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
|
2018-07-13 05:26:08 +02:00
|
|
|
$try_statement_actions = self::getFinalControlActions($stmt->stmts, $exit_functions, $in_switch);
|
2017-11-28 06:25:21 +01:00
|
|
|
|
|
|
|
if ($stmt->catches) {
|
|
|
|
$all_same = count($try_statement_actions) === 1;
|
|
|
|
|
2016-06-21 00:10:24 +02:00
|
|
|
foreach ($stmt->catches as $catch) {
|
2018-07-13 05:26:08 +02:00
|
|
|
$catch_actions = self::getFinalControlActions($catch->stmts, $exit_functions, $in_switch);
|
2017-11-28 06:25:21 +01:00
|
|
|
|
|
|
|
$all_same = $all_same && $try_statement_actions == $catch_actions;
|
|
|
|
|
|
|
|
if (!$all_same) {
|
|
|
|
$control_actions = array_merge($control_actions, $catch_actions);
|
2016-06-21 00:10:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-02 22:57:40 +01:00
|
|
|
if ($all_same && $try_statement_actions !== [self::ACTION_NONE]) {
|
2017-11-28 06:25:21 +01:00
|
|
|
return $try_statement_actions;
|
|
|
|
}
|
2016-06-21 00:10:24 +02:00
|
|
|
}
|
|
|
|
|
2018-01-29 01:02:31 +01:00
|
|
|
if ($stmt->finally) {
|
|
|
|
if ($stmt->finally->stmts) {
|
|
|
|
$finally_statement_actions = self::getFinalControlActions(
|
|
|
|
$stmt->finally->stmts,
|
2018-07-13 05:26:08 +02:00
|
|
|
$exit_functions,
|
2018-06-17 02:01:33 +02:00
|
|
|
$in_switch
|
2018-01-29 01:02:31 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (!in_array(self::ACTION_NONE, $finally_statement_actions, true)) {
|
|
|
|
return $finally_statement_actions;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$stmt->catches && !in_array(self::ACTION_NONE, $try_statement_actions, true)) {
|
|
|
|
return $try_statement_actions;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-28 06:25:21 +01:00
|
|
|
$control_actions = array_merge($control_actions, $try_statement_actions);
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-02 22:57:40 +01:00
|
|
|
$control_actions[] = self::ACTION_NONE;
|
2017-11-28 06:25:21 +01:00
|
|
|
|
|
|
|
return array_unique($control_actions);
|
2016-06-20 22:18:31 +02:00
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
2016-12-17 06:48:31 +01:00
|
|
|
* @param array<PhpParser\Node> $stmts
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
* @return bool
|
|
|
|
*/
|
2016-06-17 00:52:12 +02:00
|
|
|
public static function onlyThrows(array $stmts)
|
|
|
|
{
|
|
|
|
if (empty($stmts)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-05-27 02:05:57 +02:00
|
|
|
for ($i = count($stmts) - 1; $i >= 0; --$i) {
|
2016-06-17 00:52:12 +02:00
|
|
|
$stmt = $stmts[$i];
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Nop) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2016-05-09 14:56:07 +02:00
|
|
|
}
|