1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-12 09:19:40 +01:00
psalm/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php

375 lines
13 KiB
PHP
Raw Normal View History

<?php
2018-11-06 03:57:36 +01:00
namespace Psalm\Internal\Analyzer;
use PhpParser;
use function count;
use function in_array;
use function end;
use function strtolower;
use function array_merge;
use function array_intersect;
use function array_unique;
/**
* @internal
*/
2018-11-06 03:57:36 +01:00
class ScopeAnalyzer
{
2020-09-20 18:54:46 +02:00
public const ACTION_END = 'END';
public const ACTION_BREAK = 'BREAK';
public const ACTION_CONTINUE = 'CONTINUE';
public const ACTION_LEAVE_SWITCH = 'LEAVE_SWITCH';
public const ACTION_NONE = 'NONE';
public const ACTION_RETURN = 'RETURN';
private const ACTIONS = [
self::ACTION_END,
self::ACTION_BREAK,
self::ACTION_CONTINUE,
self::ACTION_LEAVE_SWITCH,
self::ACTION_NONE,
self::ACTION_RETURN
];
2016-11-02 07:29:00 +01:00
/**
* @param array<PhpParser\Node\Stmt> $stmts
2017-05-27 02:16:18 +02:00
*
*/
public static function doesEverBreak(array $stmts): bool
{
if (empty($stmts)) {
return false;
}
2017-05-27 02:05:57 +02:00
for ($i = count($stmts) - 1; $i >= 0; --$i) {
$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
* @param bool $return_is_exit Exit and Throw statements are treated differently from return if this is false
* @param list<'loop'|'switch'> $break_types
2017-05-27 02:16:18 +02:00
*
* @return list<value-of<self::ACTIONS>>
2016-11-02 07:29:00 +01:00
*/
public static function getFinalControlActions(
array $stmts,
?\Psalm\Internal\Provider\NodeDataProvider $nodes,
array $exit_functions,
array $break_types = [],
$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
}
$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_ ||
($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Exit_)
2016-06-21 00:10:24 +02:00
) {
if (!$return_is_exit && $stmt instanceof PhpParser\Node\Stmt\Return_) {
return [self::ACTION_RETURN];
}
return [self::ACTION_END];
}
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
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];
}
// This allows calls to functions that always exit to act as exit statements themselves
if ($nodes
&& ($stmt_expr_type = $nodes->getType($stmt->expr))
&& $stmt_expr_type->isNever()
) {
return [self::ACTION_END];
}
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;
}
if ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
if ($break_types
&& end($break_types) === 'switch'
&& (!$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];
}
return [self::ACTION_CONTINUE];
}
if ($stmt instanceof PhpParser\Node\Stmt\Break_) {
if ($break_types
&& end($break_types) === 'switch'
2018-06-17 02:01:33 +02:00
&& (!$stmt->num || !$stmt->num instanceof PhpParser\Node\Scalar\LNumber || $stmt->num->value < 2)
) {
return [self::ACTION_LEAVE_SWITCH];
}
return [self::ACTION_BREAK];
2016-06-20 22:18:31 +02:00
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
$if_statement_actions = self::getFinalControlActions(
$stmt->stmts,
$nodes,
$exit_functions,
$break_types
);
$else_statement_actions = $stmt->else
? self::getFinalControlActions($stmt->else->stmts, $nodes, $exit_functions, $break_types)
: [];
2016-06-20 22:18:31 +02: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) {
$elseif_control_actions = self::getFinalControlActions(
$elseif->stmts,
$nodes,
$exit_functions,
$break_types
);
$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
}
}
}
2016-06-20 22:18:31 +02:00
if ($all_same) {
return $if_statement_actions;
2016-06-20 22:18:31 +02: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_) {
$has_ended = false;
$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
for ($d = count($stmt->cases) - 1; $d >= 0; --$d) {
$case = $stmt->cases[$d];
2016-06-20 22:18:31 +02:00
$case_actions = self::getFinalControlActions($case->stmts, $nodes, $exit_functions, ['switch']);
2018-06-17 02:01:33 +02:00
if (array_intersect([
self::ACTION_LEAVE_SWITCH,
self::ACTION_BREAK,
self::ACTION_CONTINUE
], $case_actions)
) {
continue 2;
2016-06-20 22:18:31 +02:00
}
if (!$case->cond) {
$has_non_breaking_default = true;
}
$case_does_end = $case_actions == [self::ACTION_END];
2016-06-28 19:56:44 +02:00
if ($case_does_end) {
$has_ended = true;
2016-06-20 22:18:31 +02:00
}
if (!$case_does_end && !$has_ended) {
continue 2;
2016-06-20 22:18:31 +02: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)) {
return [self::ACTION_END];
}
2016-06-20 22:18:31 +02:00
}
if ($stmt instanceof PhpParser\Node\Stmt\Do_
|| $stmt instanceof PhpParser\Node\Stmt\While_
|| $stmt instanceof PhpParser\Node\Stmt\Foreach_
|| $stmt instanceof PhpParser\Node\Stmt\For_
) {
$do_actions = self::getFinalControlActions(
$stmt->stmts,
$nodes,
$exit_functions,
array_merge($break_types, ['loop'])
);
$control_actions = array_merge($control_actions, $do_actions);
2016-06-21 00:10:24 +02:00
}
if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
$try_statement_actions = self::getFinalControlActions(
$stmt->stmts,
$nodes,
$exit_functions,
$break_types
);
if ($stmt->catches) {
$all_same = count($try_statement_actions) === 1;
2016-06-21 00:10:24 +02:00
foreach ($stmt->catches as $catch) {
$catch_actions = self::getFinalControlActions(
$catch->stmts,
$nodes,
$exit_functions,
$break_types
);
$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
}
}
if ($all_same && $try_statement_actions !== [self::ACTION_NONE]) {
return $try_statement_actions;
}
2016-06-21 00:10:24 +02:00
}
if ($stmt->finally) {
if ($stmt->finally->stmts) {
$finally_statement_actions = self::getFinalControlActions(
$stmt->finally->stmts,
$nodes,
$exit_functions,
$break_types
);
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;
}
}
$control_actions = \array_merge($control_actions, $try_statement_actions);
2016-06-20 22:18:31 +02:00
}
}
$control_actions[] = self::ACTION_NONE;
2020-01-27 18:26:06 +01:00
return \array_values(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
*/
public static function onlyThrowsOrExits(\Psalm\NodeTypeProvider $type_provider, array $stmts): bool
{
if (empty($stmts)) {
return false;
}
2017-05-27 02:05:57 +02:00
for ($i = count($stmts) - 1; $i >= 0; --$i) {
$stmt = $stmts[$i];
if ($stmt instanceof PhpParser\Node\Stmt\Throw_
|| ($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\Exit_)
) {
return true;
}
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
$stmt_type = $type_provider->getType($stmt->expr);
if ($stmt_type && $stmt_type->isNever()) {
return true;
}
}
}
return false;
}
}