Page MenuHomeWickedGov Phorge

UseReturnValueVisitor.php
No OneTemporary

Size
20 KB
Referenced Files
None
Subscribers
None

UseReturnValueVisitor.php

<?php
declare(strict_types=1);
namespace Phan\Plugin\Internal\UseReturnValuePlugin;
use ast;
use ast\Node;
use Exception;
use Phan\Analysis\BlockExitStatusChecker;
use Phan\AST\ASTReverter;
use Phan\AST\ContextNode;
use Phan\Config;
use Phan\Exception\CodeBaseException;
use Phan\Language\Element\Func;
use Phan\Language\Element\FunctionInterface;
use Phan\Language\Element\Method;
use Phan\Plugin\Internal\UseReturnValuePlugin;
use Phan\PluginV3\PluginAwarePostAnalysisVisitor;
use function is_array;
use function iterator_to_array;
/**
* 1. Checks for invocations of functions/methods where the return value should be used.
* Also, gathers statistics on how often those functions/methods are used.
* 2. Checks for invocations of functions/methods that return 'never' but the return value gets used.
* e.g. $x = alwaysExits();
*
* @phan-file-suppress PhanAccessPropertyInternal
*/
class UseReturnValueVisitor extends PluginAwarePostAnalysisVisitor
{
/** @var list<Node> set by plugin framework */
protected $parent_node_list;
/**
* Skip unary ops when determining the parent node.
* E.g. for `@foo();`, the parent node is AST_STMT_LIST (which we infer means the result is unused)
* For `$x = +foo();` the parent node is AST_ASSIGN.
* @return array{0:?Node,1:bool} - [$parent, $used]
* $used is whether the expression is used - it should only be checked if the parent is known.
*/
private function findNonUnaryParentNode(Node $node): array
{
$parent = \end($this->parent_node_list);
if (!$parent) {
return [null, true];
}
while (true) {
switch ($parent->kind) {
case ast\AST_UNARY_OP:
break;
case ast\AST_CONDITIONAL:
if ($node === $parent->children['cond']) {
return [null, true];
}
break;
case ast\AST_BINARY_OP:
if (!\in_array($parent->flags, [ast\flags\BINARY_COALESCE, ast\flags\BINARY_BOOL_OR, ast\flags\BINARY_BOOL_AND], true) || $node !== $parent->children['right']) {
return [null, true];
}
break;
default:
break 2;
}
$node = $parent;
$parent = \prev($this->parent_node_list);
if (!$parent) {
return [null, true];
}
}
// @phan-suppress-next-line PhanPluginUnreachableCode Phan can't analyze `break 2;`
switch ($parent->kind) {
case ast\AST_STMT_LIST:
return [$parent, false];
case ast\AST_EXPR_LIST:
return [$parent, $this->isUsedExpressionInExprList($node, $parent)];
}
return [$parent, true];
}
/**
* Find the parent node for the purpose of checking if the usage is safe
*
* @return array{0:?Node,1:bool} - [$parent, $used]
* $used is whether the expression is used - it should only be checked if the parent is known.
*
* (Also return the parent node to make debugging easier)
*/
private function findParentNodeForCheckingNeverType(Node $node): array
{
$parent = \end($this->parent_node_list);
if (!$parent) {
return [null, true];
}
while (true) {
switch ($parent->kind) {
case ast\AST_CONDITIONAL:
return [$parent, $node === $parent->children['cond']];
case ast\AST_BINARY_OP:
return [$parent, !\in_array($parent->flags, [ast\flags\BINARY_COALESCE, ast\flags\BINARY_BOOL_OR, ast\flags\BINARY_BOOL_AND], true) || $node !== $parent->children['right']];
default:
break 2;
}
$node = $parent;
$parent = \prev($this->parent_node_list);
if (!$parent) {
return [null, true];
}
}
// @phan-suppress-next-line PhanPluginUnreachableCode Phan can't analyze `break 2;`
switch ($parent->kind) {
case ast\AST_STMT_LIST:
return [$parent, false];
case ast\AST_EXPR_LIST:
return [$parent, $this->isUsedExpressionInExprList($node, $parent)];
}
return [$parent, true];
}
private function isUsedExpressionInExprList(Node $node, Node $parent): bool
{
return $node === \end($parent->children) && $parent === (\prev($this->parent_node_list)->children['cond'] ?? null);
}
public function visitExit(Node $node): void
{
$used = $this->findParentNodeForCheckingNeverType($node)[1];
if (!$used) {
return;
}
// To reduce false positives, only emit this issue if all functions have the same return type
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
UseReturnValuePlugin::UseReturnValueOfNever,
"Saw use of value of expression {CODE} which likely uses the {CODE} statement with a return type of '{TYPE}' - this will not return normally",
[ASTReverter::toShortString($node), 'exit', 'never']
);
}
/**
* @param Node $node a node of type AST_CALL
* @override
*/
public function visitCall(Node $node): void
{
$used = $this->findNonUnaryParentNode($node)[1];
if (!$used && $node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
$this->warnUnusedCallableConvert($node);
return;
}
//fwrite(STDERR, "Saw parent of type " . ast\get_kind_name($parent->kind) . "\n");
$expression = $node->children['expr'];
try {
$function_list = (new ContextNode(
$this->code_base,
$this->context,
$expression
))->getFunctionFromNode();
// Convert the generator to an array so that iterating over it will not consume the elements
if (!is_array($function_list)) {
$function_list = iterator_to_array($function_list, false);
}
$this->checkIfUsingFunctionThatNeverReturns($function_list, $node);
if ($used && !UseReturnValuePlugin::$use_dynamic) {
return;
}
foreach ($function_list as $function) {
$this->checkUseReturnValueGenerator($function, $node);
if ($function instanceof Method) {
$fqsen = $function->getDefiningFQSEN()->__toString();
} else {
$fqsen = $function->getFQSEN()->__toString();
}
if (!UseReturnValuePlugin::$use_dynamic) {
$this->quickWarn($function, $fqsen, $node);
continue;
}
if ($function instanceof Func && $function->isClosure()) {
continue;
}
$counter = UseReturnValuePlugin::$stats[$fqsen] ?? null;
if (!$counter) {
UseReturnValuePlugin::$stats[$fqsen] = $counter = new StatsForFQSEN($function);
}
$key = $this->context->__toString();
if ($used) {
$counter->used_locations[$key] = $this->context;
} else {
$counter->unused_locations[$key] = $this->context;
}
}
} catch (CodeBaseException $_) {
}
}
/**
* @param list<FunctionInterface> $function_list
*/
private function checkIfUsingFunctionThatNeverReturns(array $function_list, Node $node): void
{
if (!$function_list) {
return;
}
if ($node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
// This isn't calling the function, it's creating a closure.
return;
}
foreach ($function_list as $function) {
if (!$function->getUnionType()->isNeverType()) {
return;
}
}
if (Config::getValue('dead_code_detection_treat_never_type_as_unreachable')) {
// We now know that this is AST_CALL, AST_STATIC_CALL, or AST_METHOD_CALL
// Mark this to affect BlockExitStatusChecker
$node->flags = (($node->flags & ~BlockExitStatusChecker::STATUS_PROCEED) | BlockExitStatusChecker::STATUS_THROW);
}
$used = $this->findParentNodeForCheckingNeverType($node)[1];
if (!$used) {
// Don't warn about $x = value() ?? exit('expected non-null')
return;
}
// To reduce false positives, only emit this issue if all functions have the same return type
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
UseReturnValuePlugin::UseReturnValueOfNever,
"Saw use of value of expression {CODE} which likely uses the function {FUNCTIONLIKE} with a return type of '{TYPE}' - this will not return normally",
[ASTReverter::toShortString($node), $function->getRepresentationForIssue(true), 'never']
);
}
/**
* Checks if a method has unnecessary branches leading to the same returned value
*
* @param Node $node a node of type AST_METHOD
* @override
*/
public function visitMethod(Node $node): void
{
$this->analyzeFunctionLike($node);
}
/**
* Checks if a global function has unnecessary branches leading to the same returned value
*
* @param Node $node a node of type AST_FUNC_DECL
* @override
*/
public function visitFuncDecl(Node $node): void
{
$this->analyzeFunctionLike($node);
}
/**
* Checks if a closure has unnecessary branches leading to the same returned value
*
* NOTE: There is no need to implement this for AST_ARROW_FUNC,
* which is currently limited to only one possible returned expression.
* @param Node $node a node of type AST_CLOSURE
* @override
*/
public function visitClosure(Node $node): void
{
$this->analyzeFunctionLike($node);
}
/**
* Checks if a function-like has unnecessary branches leading to the same returned value
*/
private function analyzeFunctionLike(Node $node): void
{
if (!$this->context->isInFunctionLikeScope()) {
return;
}
$method = $this->context->getFunctionLikeInScope($this->code_base);
if (!$method->isPure()) {
return;
}
if ($method instanceof Method) {
if ($method->isAbstract()) {
return;
}
}
if (!$method->hasReturn() || $method->hasYield()) {
return;
}
$stmts = $node->children['stmts'];
if ($stmts instanceof Node) {
(new RedundantReturnVisitor($this->code_base, $this->context, $stmts))->analyze();
}
}
/**
* @param Node $node a node of type AST_NULLSAFE_METHOD_CALL
* @override
*/
public function visitNullsafeMethodCall(Node $node): void
{
$this->visitMethodCall($node);
}
/**
* @param Node $node a node of type AST_METHOD_CALL
* @override
*/
public function visitMethodCall(Node $node): void
{
[$parent, $used] = $this->findNonUnaryParentNode($node);
if (!$parent) {
//fwrite(STDERR, "No parent in " . __METHOD__ . "\n");
return;
}
if (!$used && $node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
$this->warnUnusedCallableConvert($node);
return;
}
$key = $this->context->getFile() . ':' . $this->context->getLineNumberStart();
//fwrite(STDERR, "Saw parent of type " . ast\get_kind_name($parent->kind) . "\n");
$method_name = $node->children['method'];
if (!\is_string($method_name)) {
return;
}
try {
$method = (new ContextNode(
$this->code_base,
$this->context,
$node
))->getMethod($method_name, false, true);
} catch (Exception $_) {
return;
}
$this->checkIfUsingFunctionThatNeverReturns([$method], $node);
if ($used && !UseReturnValuePlugin::$use_dynamic) {
return;
}
$this->checkUseReturnValueGenerator($method, $node);
$fqsen = $method->getDefiningFQSEN()->__toString();
if (!UseReturnValuePlugin::$use_dynamic) {
$this->quickWarn($method, $fqsen, $node);
return;
}
$counter = UseReturnValuePlugin::$stats[$fqsen] ?? null;
if (!$counter) {
UseReturnValuePlugin::$stats[$fqsen] = $counter = new StatsForFQSEN($method);
}
if ($used) {
$counter->used_locations[$key] = $this->context;
} else {
$counter->unused_locations[$key] = $this->context;
}
}
protected function warnUnusedCallableConvert(Node $node): void
{
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
UseReturnValuePlugin::UseReturnValueCallableConvert,
'Expected to use the Closure created from {CODE}',
[ASTReverter::toShortString($node)]
);
}
/**
* @param Node $node a node of type AST_STATIC_CALL
* @override
*/
public function visitStaticCall(Node $node): void
{
[$parent, $used] = $this->findNonUnaryParentNode($node);
if (!$parent) {
//fwrite(STDERR, "No parent in " . __METHOD__ . "\n");
return;
}
//fwrite(STDERR, "Saw parent of type " . ast\get_kind_name($parent->kind) . "\n");
if (!$used && $node->children['args']->kind === ast\AST_CALLABLE_CONVERT) {
$this->warnUnusedCallableConvert($node);
return;
}
$method_name = $node->children['method'];
if (!\is_string($method_name)) {
return;
}
try {
$method = (new ContextNode(
$this->code_base,
$this->context,
$node
))->getMethod($method_name, true, true);
} catch (Exception $_) {
return;
}
$this->checkIfUsingFunctionThatNeverReturns([$method], $node);
if ($used && !UseReturnValuePlugin::$use_dynamic) {
return;
}
$this->checkUseReturnValueGenerator($method, $node);
$fqsen = $method->getDefiningFQSEN()->__toString();
if ($this->quickWarn($method, $fqsen, $node)) {
return;
}
if (!UseReturnValuePlugin::$use_dynamic) {
return;
}
$counter = UseReturnValuePlugin::$stats[$fqsen] ?? null;
if (!$counter) {
UseReturnValuePlugin::$stats[$fqsen] = $counter = new StatsForFQSEN($method);
}
$key = $this->context->getFile() . ':' . $this->context->getLineNumberStart();
if ($used) {
$counter->used_locations[$key] = $this->context;
} else {
$counter->unused_locations[$key] = $this->context;
}
}
private function checkUseReturnValueGenerator(FunctionInterface $function, Node $node): void
{
if ($function->hasYield() || $function->getUnionType()->isExclusivelyGenerators()) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
UseReturnValuePlugin::UseReturnValueGenerator,
'Expected to use the return value of the function/method {FUNCTION} returning a generator of type {TYPE}',
[$function->getRepresentationForIssue(true), $function->getUnionType()]
);
}
}
private static function isSecondArgumentEqualToConst(Node $node, string $const_name): bool
{
$args = $node->children['args']->children;
$bool_node = $args[1] ?? null;
if (!$bool_node instanceof Node) {
if ($const_name === 'true') {
return (bool)$bool_node;
} elseif ($const_name === 'false') {
return (bool)$bool_node;
}
return false;
}
if ($bool_node->kind !== ast\AST_CONST) {
return false;
}
$name = $bool_node->children['name']->children['name'] ?? null;
return \is_string($name) && \strcasecmp($name, $const_name) === 0;
}
/**
* @return bool true if $fqsen_key should be treated as if it were read-only.
* Precondition: $fqsen_key is found as a special case in this plugin's set of functions.
*/
public static function doesSpecialCaseHaveSideEffects(string $fqsen_key, Node $node): bool
{
switch ($fqsen_key) {
case 'var_export':
case 'print_r':
// var_export and print_r take a second bool argument.
// Warn if that argument is true.
return !self::isSecondArgumentEqualToConst($node, 'true');
case 'class_exists':
case 'interface_exists':
case 'trait_exists':
// Triggers autoloader unless second argument is false
return !self::isSecondArgumentEqualToConst($node, 'false');
case 'preg_match':
case 'preg_match_all':
return \count($node->children['args']->children) >= 3;
}
return true;
}
private function shouldNotWarnForSpecialCase(string $fqsen_key, Node $node): bool
{
switch ($fqsen_key) {
case 'call_user_func':
case 'call_user_func_array':
return $this->shouldNotWarnForDynamicCall($node->children['args']->children[0] ?? null);
default:
return self::doesSpecialCaseHaveSideEffects($fqsen_key, $node);
}
}
/**
* @param ?(Node|string|int|float) $node_name
*/
private function shouldNotWarnForDynamicCall($node_name): bool
{
if ($node_name instanceof Node) {
foreach ((new ContextNode(
$this->code_base,
$this->context,
$node_name
))->getFunctionFromNode() as $function) {
$node_name = $function->getFQSEN()->__toString();
break;
}
}
if (!\is_string($node_name)) {
return true;
}
$fqsen_key = \strtolower(\ltrim($node_name, "\\"));
return (UseReturnValuePlugin::HARDCODED_FQSENS[$fqsen_key] ?? null) !== true;
}
/**
* @return bool true if there is no need to perform dynamic checks later
*/
private function quickWarn(FunctionInterface $method, string $fqsen, Node $node): bool
{
if (!$method->isPure()) {
$fqsen_key = \strtolower(\ltrim($fqsen, "\\"));
$result = UseReturnValuePlugin::HARDCODED_FQSENS[$fqsen_key] ?? null;
if (!$result) {
return $result ?? true;
}
if ($result === UseReturnValuePlugin::SPECIAL_CASE) {
if ($this->shouldNotWarnForSpecialCase($fqsen_key, $node)) {
return false;
}
}
}
if ($method->isPHPInternal()) {
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
UseReturnValuePlugin::UseReturnValueInternalKnown,
'Expected to use the return value of the internal function/method {FUNCTION}',
[$fqsen]
);
return true;
}
$phpdoc_return_type = $method->getPHPDocReturnType();
if (($phpdoc_return_type && $phpdoc_return_type->isNull()) || $method->getRealReturnType()->isNull() || !($method->hasReturn() || $method->isFromPHPDoc())) {
return false;
}
$this->emitPluginIssue(
$this->code_base,
(clone $this->context)->withLineNumberStart($node->lineno),
UseReturnValuePlugin::UseReturnValueKnown,
'Expected to use the return value of the user-defined function/method {FUNCTION} defined at {FILE}:{LINE}',
[$method->getRepresentationForIssue(), $method->getContext()->getFile(), $method->getContext()->getLineNumberStart()]
);
return true;
}
}
// Every plugin needs to return an instance of itself at the
// end of the file in which it's defined.

File Metadata

Mime Type
text/x-php
Expires
Sat, May 16, 21:46 (1 d, 6 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
2e/20/2f426637768638555b75429721f6
Default Alt Text
UseReturnValueVisitor.php (20 KB)

Event Timeline