1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-03 10:07:52 +01:00
psalm/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php

447 lines
16 KiB
PHP

<?php
namespace Psalm\Internal\Analyzer;
use PhpParser;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\NodeTypeProvider;
use function array_diff;
use function array_filter;
use function array_intersect;
use function array_unique;
use function array_values;
use function count;
use function in_array;
/**
* @internal
*/
final class ScopeAnalyzer
{
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_LEAVE_LOOP = 'LEAVE_LOOP';
public const ACTION_NONE = 'NONE';
public const ACTION_RETURN = 'RETURN';
/**
* @param array<PhpParser\Node> $stmts
* @param list<'loop'|'switch'> $break_types
* @param bool $return_is_exit Exit and Throw statements are treated differently from return if this is false
* @return list<self::ACTION_*>
* @psalm-suppress ComplexMethod nothing much we can do
*/
public static function getControlActions(
array $stmts,
?NodeDataProvider $nodes,
array $break_types,
bool $return_is_exit = true
): array {
if (empty($stmts)) {
return [self::ACTION_NONE];
}
$control_actions = [];
foreach ($stmts as $stmt) {
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_)
) {
if (!$return_is_exit && $stmt instanceof PhpParser\Node\Stmt\Return_) {
$stmt_return_type = null;
if ($nodes && $stmt->expr) {
$stmt_return_type = $nodes->getType($stmt->expr);
}
// don't consider a return if the expression never returns (e.g. a throw inside a short closure)
if ($stmt_return_type && $stmt_return_type->isNever()) {
return array_values(array_unique([...$control_actions, ...[self::ACTION_END]]));
}
return array_values(array_unique([...$control_actions, ...[self::ACTION_RETURN]]));
}
return array_values(array_unique([...$control_actions, ...[self::ACTION_END]]));
}
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
// 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 array_values(array_unique([...$control_actions, ...[self::ACTION_END]]));
}
continue;
}
if ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
$count = !$stmt->num
? 1
: ($stmt->num instanceof PhpParser\Node\Scalar\LNumber ? $stmt->num->value : null);
if ($break_types && $count !== null && count($break_types) >= $count) {
/** @psalm-suppress InvalidArrayOffset Some int-range improvements are needed */
if ($break_types[count($break_types) - $count] === 'switch') {
return [...$control_actions, ...[self::ACTION_LEAVE_SWITCH]];
}
return array_values($control_actions);
}
return array_values(array_unique([...$control_actions, ...[self::ACTION_CONTINUE]]));
}
if ($stmt instanceof PhpParser\Node\Stmt\Break_) {
$count = !$stmt->num
? 1
: ($stmt->num instanceof PhpParser\Node\Scalar\LNumber ? $stmt->num->value : null);
if ($break_types && $count !== null && count($break_types) >= $count) {
/** @psalm-suppress InvalidArrayOffset Some int-range improvements are needed */
if ($break_types[count($break_types) - $count] === 'switch') {
return [...$control_actions, ...[self::ACTION_LEAVE_SWITCH]];
}
/** @psalm-suppress InvalidArrayOffset Some int-range improvements are needed */
if ($break_types[count($break_types) - $count] === 'loop') {
return [...$control_actions, ...[self::ACTION_LEAVE_LOOP]];
}
return array_values($control_actions);
}
return array_values(array_unique([...$control_actions, ...[self::ACTION_BREAK]]));
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
$if_statement_actions = self::getControlActions(
$stmt->stmts,
$nodes,
$break_types,
$return_is_exit,
);
$all_leave = !array_filter(
$if_statement_actions,
static fn(string $action): bool => $action === self::ACTION_NONE,
);
$else_statement_actions = $stmt->else
? self::getControlActions(
$stmt->else->stmts,
$nodes,
$break_types,
$return_is_exit,
) : [];
$all_leave = $all_leave
&& $else_statement_actions
&& !array_filter(
$else_statement_actions,
static fn(string $action): bool => $action === self::ACTION_NONE,
);
$all_elseif_actions = [];
if ($stmt->elseifs) {
foreach ($stmt->elseifs as $elseif) {
$elseif_control_actions = self::getControlActions(
$elseif->stmts,
$nodes,
$break_types,
$return_is_exit,
);
$all_leave = $all_leave
&& !array_filter(
$elseif_control_actions,
static fn(string $action): bool => $action === self::ACTION_NONE,
);
$all_elseif_actions = [...$elseif_control_actions, ...$all_elseif_actions];
}
}
if ($all_leave) {
return array_values(
array_unique([
...$control_actions,
...$if_statement_actions,
...$else_statement_actions,
...$all_elseif_actions,
]),
);
}
$control_actions = array_filter(
[...$control_actions, ...$if_statement_actions, ...$else_statement_actions, ...$all_elseif_actions],
static fn(string $action): bool => $action !== self::ACTION_NONE,
);
}
if ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
$has_ended = false;
$has_non_breaking_default = false;
$has_default_terminator = false;
$all_case_actions = [];
// iterate backwards in a case statement
for ($d = count($stmt->cases) - 1; $d >= 0; --$d) {
$case = $stmt->cases[$d];
$case_actions = self::getControlActions(
$case->stmts,
$nodes,
[...$break_types, ...['switch']],
$return_is_exit,
);
if (array_intersect([
self::ACTION_LEAVE_SWITCH,
self::ACTION_BREAK,
self::ACTION_CONTINUE,
], $case_actions)
) {
continue 2;
}
if (!$case->cond) {
$has_non_breaking_default = true;
}
$case_does_end = !array_diff(
$control_actions,
[self::ACTION_END, self::ACTION_RETURN],
);
if ($case_does_end) {
$has_ended = true;
}
$all_case_actions = [...$all_case_actions, ...$case_actions];
if (!$case_does_end && !$has_ended) {
continue 2;
}
if ($has_non_breaking_default && $case_does_end) {
$has_default_terminator = true;
}
}
$all_case_actions = array_filter(
$all_case_actions,
static fn(string $action): bool => $action !== self::ACTION_NONE,
);
if ($has_default_terminator || $stmt->getAttribute('allMatched', false)) {
return array_values(array_unique([...$control_actions, ...$all_case_actions]));
}
$control_actions = [...$control_actions, ...$all_case_actions];
}
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_
) {
$loop_actions = self::getControlActions(
$stmt->stmts,
$nodes,
[...$break_types, ...['loop']],
$return_is_exit,
);
$control_actions = array_filter(
[...$control_actions, ...$loop_actions],
static fn(string $action): bool => $action !== self::ACTION_NONE,
);
if (($stmt instanceof PhpParser\Node\Stmt\While_
|| $stmt instanceof PhpParser\Node\Stmt\Do_)
&& $nodes
&& ($stmt_expr_type = $nodes->getType($stmt->cond))
&& $stmt_expr_type->isAlwaysTruthy()
&& !in_array(self::ACTION_LEAVE_LOOP, $control_actions, true)
) {
//infinite while loop that only return don't have an exit path
$have_exit_path = (bool)array_diff(
$control_actions,
[self::ACTION_END, self::ACTION_RETURN],
);
if (!$have_exit_path) {
return array_values(array_unique($control_actions));
}
}
if ($stmt instanceof PhpParser\Node\Stmt\For_
&& $nodes
&& !in_array(self::ACTION_LEAVE_LOOP, $control_actions, true)
) {
$is_infinite_loop = true;
if ($stmt->cond) {
foreach ($stmt->cond as $cond) {
$stmt_expr_type = $nodes->getType($cond);
if (!$stmt_expr_type || !$stmt_expr_type->isAlwaysTruthy()) {
$is_infinite_loop = false;
}
}
}
if ($is_infinite_loop) {
//infinite while loop that only return don't have an exit path
$have_exit_path = (bool)array_diff(
$control_actions,
[self::ACTION_END, self::ACTION_RETURN],
);
if (!$have_exit_path) {
return array_values(array_unique($control_actions));
}
}
}
$control_actions = array_filter(
$control_actions,
static fn(string $action): bool => $action !== self::ACTION_LEAVE_LOOP,
);
}
if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
$try_statement_actions = self::getControlActions(
$stmt->stmts,
$nodes,
$break_types,
$return_is_exit,
);
$try_leaves = !array_filter(
$try_statement_actions,
static fn(string $action): bool => $action === self::ACTION_NONE,
);
$all_catch_actions = [];
if ($stmt->catches) {
$all_leave = $try_leaves;
foreach ($stmt->catches as $catch) {
$catch_actions = self::getControlActions(
$catch->stmts,
$nodes,
$break_types,
$return_is_exit,
);
$all_leave = $all_leave
&& !array_filter(
$catch_actions,
static fn(string $action): bool => $action === self::ACTION_NONE,
);
if (!$all_leave) {
$control_actions = [...$control_actions, ...$catch_actions];
} else {
$all_catch_actions = [...$all_catch_actions, ...$catch_actions];
}
}
if ($all_leave && $try_statement_actions !== [self::ACTION_NONE]) {
return array_values(
array_unique(
[...$control_actions, ...$try_statement_actions, ...$all_catch_actions],
),
);
}
} elseif ($try_leaves) {
return array_values(array_unique([...$control_actions, ...$try_statement_actions]));
}
if ($stmt->finally && $stmt->finally->stmts) {
$finally_statement_actions = self::getControlActions(
$stmt->finally->stmts,
$nodes,
$break_types,
$return_is_exit,
);
if (!in_array(self::ACTION_NONE, $finally_statement_actions, true)) {
return [...array_filter(
$control_actions,
static fn(string $action): bool => $action !== self::ACTION_NONE,
), ...$finally_statement_actions];
}
}
$control_actions = array_filter(
[...$control_actions, ...$try_statement_actions],
static fn(string $action): bool => $action !== self::ACTION_NONE,
);
}
}
$control_actions[] = self::ACTION_NONE;
return array_values(array_unique($control_actions));
}
/**
* @param array<PhpParser\Node> $stmts
*/
public static function onlyThrowsOrExits(NodeTypeProvider $type_provider, array $stmts): bool
{
if (empty($stmts)) {
return false;
}
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;
}
/**
* @param array<PhpParser\Node> $stmts
*/
public static function onlyThrows(array $stmts): bool
{
$stmts_count = count($stmts);
if ($stmts_count !== 1) {
return false;
}
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
return true;
}
}
return false;
}
}