src/Phan/Plugin/Internal/UseReturnValuePlugin/RedundantReturnVisitor.php
<?php
declare(strict_types=1);
namespace Phan\Plugin\Internal\UseReturnValuePlugin;
use ast;
use ast\Node;
use Phan\Analysis\BlockExitStatusChecker;
use Phan\AST\ASTHasher;
use Phan\AST\ASTReverter;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Exception\IssueException;
use Phan\Issue;
use Phan\Language\Context;
use Phan\Parse\ParseVisitor;
/**
* Checks for function-likes that have unnecessary branches to equivalent return statements.
*
* This does not handle returning variables, and is only run for functions inferred to be pure.
*
* @phan-file-suppress PhanAccessPropertyInternal
*/
class RedundantReturnVisitor
{
/** @var CodeBase */
private $code_base;
/** @var Context */
private $context;
/** @var Node */
private $stmts;
public function __construct(CodeBase $code_base, Context $context, Node $stmts)
{
$this->code_base = $code_base;
$this->context = $context;
$this->stmts = $stmts;
}
/**
* Check for any code paths where 2 or more return statements would return the same value
*/
public function analyze(): void
{
try {
$this->analyzeNode($this->stmts);
} catch (IssueException $e) {
Issue::maybeEmitInstance(
$this->code_base,
$this->context,
$e->getIssueInstance()
);
}
}
/**
* @return list<Node>
* @throws IssueException for the first issue seen in this function-like, if any
* TODO: Instead, iterate backwards through the AST_STMT_LIST to check if the last group of returns and second-last group of returns are redundant.
*/
private function analyzeNode(Node $stmts): array
{
$kind = $stmts->kind;
switch ($kind) {
// Nodes that create new scopes
case ast\AST_FUNC_DECL:
case ast\AST_CLASS:
case ast\AST_CLOSURE:
case ast\AST_METHOD:
// Nodes that can't contain return statements.
case ast\AST_CALL:
case ast\AST_PROP:
case ast\AST_NULLSAFE_PROP:
case ast\AST_STATIC_PROP:
case ast\AST_STATIC_CALL:
case ast\AST_METHOD_CALL:
case ast\AST_NULLSAFE_METHOD_CALL:
case ast\AST_UNARY_OP:
case ast\AST_BINARY_OP:
case ast\AST_ASSIGN:
case ast\AST_ASSIGN_OP:
case ast\AST_ECHO:
case ast\AST_ARROW_FUNC:
return [];
case ast\AST_RETURN:
return [$stmts];
}
$children = $stmts->children;
if (!$children) {
return [];
}
$groups = [];
foreach ($children as $child) {
if (!$child instanceof Node) {
continue;
}
$return_group = $this->analyzeNode($child);
if ($return_group) {
$groups[] = $return_group;
}
}
if ($groups) {
if (\count($groups) > 1) {
$possible_return_nodes = \array_merge(...$groups);
} else {
return $groups[0];
}
} else {
return [];
}
// Handle node kinds that are statement lists or cause branches containing multiple statement lists.
if (!\in_array($kind, [ast\AST_STMT_LIST, ast\AST_IF, ast\AST_SWITCH_LIST], true)) {
return $possible_return_nodes;
}
if (\count($possible_return_nodes) === 0) {
return $possible_return_nodes;
}
if (\count($possible_return_nodes) <= 1 && ($stmts !== $this->stmts || \count($possible_return_nodes) === 0)) {
return $possible_return_nodes;
}
$exit_status = (new BlockExitStatusChecker())->__invoke($stmts);
if ($exit_status !== BlockExitStatusChecker::STATUS_RETURN) {
if ($stmts !== $this->stmts || !($exit_status & BlockExitStatusChecker::STATUS_PROCEED)) {
return $possible_return_nodes;
}
// This is the function body, and there's one code path where it will not return a value.
$line = $this->stmts->lineno;
$possible_return_nodes[] = new Node(ast\AST_RETURN, 0, [
'expr' => new Node(ast\AST_CONST, 0, [
'name' => new Node(ast\AST_NAME, ast\flags\NAME_NOT_FQ, ['name' => 'null'], $line)
], $line)
], $line);
}
// There are 2 or more possible returned statements. Check if all returned expressions are the same.
// @phan-suppress-next-line PhanPartialTypeMismatchArgument can't understand count() assertions
if (\count($groups) > 2 && $kind !== ast\AST_SWITCH_LIST) {
// e.g. warn about the last two groups of returns being the same, for examples such as the following:
//
// - if (c1) { return true; }
// elseif (c2) { return false; }
// else { return false; }
// - if (c1) { return true; }
// if (c2) { return false; }
// return false;
//
// but don't warn about switches unless every single one of the cases returns the same thing,
// as a style choice.
$this->checkMultipleReturns(\array_merge(...\array_slice($groups, -2)));
} else {
$this->checkMultipleReturns($possible_return_nodes);
}
return $possible_return_nodes;
}
/**
* @param non-empty-list<Node> $possible_return_nodes
* @throws IssueException for the first issue seen in this function-like, if any
*/
private function checkMultipleReturns(array $possible_return_nodes): void
{
if (\count($possible_return_nodes) <= 1) {
return;
}
$remaining_returns = $possible_return_nodes;
$last_return = \array_pop($remaining_returns);
$last_expr = $last_return->children['expr'];
if (!ParseVisitor::isConstExpr($last_expr)) {
return;
}
$last_hash = ASTHasher::hash($last_expr);
$resolved_last_value = false;
$last_value = null;
foreach ($remaining_returns as $return) {
$expr = $return->children['expr'];
if (!ParseVisitor::isConstExpr($expr)) {
return;
}
if (ASTHasher::hash($expr) === $last_hash) {
continue;
}
if (!$resolved_last_value) {
if ($last_expr instanceof Node) {
$last_value = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $last_expr)->asSingleScalarValueOrNullOrSelf();
} else {
// null/string/int/float
$last_value = $last_expr;
}
if (\is_object($last_value)) {
return;
}
$resolved_last_value = true;
}
if ($expr instanceof Node) {
$value = UnionTypeVisitor::unionTypeFromNode($this->code_base, $this->context, $expr)->asSingleScalarValueOrNullOrSelf();
} else {
// null/string/int/float
$value = $expr;
}
if ($value !== $last_value) {
return;
}
// This is the same value as the previous return, e.g. `return 1+1;` and `return 2;`
}
throw new IssueException(
Issue::fromType(Issue::UnusedReturnBranchWithoutSideEffects)(
$this->context->getFile(),
$last_return->lineno,
[ASTReverter::toShortString($last_expr), \reset($remaining_returns)->lineno ?? 0]
)
);
}
}