src/Phan/Plugin/Internal/UseReturnValuePlugin/UseReturnValueVisitor.php
<?php
declare(strict_types=1);
namespace Phan\Plugin\Internal\UseReturnValuePlugin;
use ast;
use ast\Node;
use Exception;
use Phan\AST\ContextNode;
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;
/**
* Checks for invocations of functions/methods where the return value should be used.
* Also, gathers statistics on how often those functions/methods are used.
* @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];
}
private function isUsedExpressionInExprList(Node $node, Node $parent): bool
{
return $node === \end($parent->children) && $parent === (\prev($this->parent_node_list)->children['cond'] ?? null);
}
/**
* @param Node $node a node of type AST_CALL
* @override
*/
public function visitCall(Node $node): void
{
[$parent, $used] = $this->findNonUnaryParentNode($node);
if (!$parent) {
//fwrite(STDERR, "No parent in " . __METHOD__ . "\n");
return;
}
if ($used && !UseReturnValuePlugin::$use_dynamic) {
return;
}
$key = $this->context->getFile() . ':' . $this->context->getLineNumberStart();
//fwrite(STDERR, "Saw parent of type " . ast\get_kind_name($parent->kind) . "\n");
$expression = $node->children['expr'];
try {
$function_list_generator = (new ContextNode(
$this->code_base,
$this->context,
$expression
))->getFunctionFromNode();
foreach ($function_list_generator 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);
}
if ($used) {
$counter->used_locations[$key] = $this->context;
} else {
$counter->unused_locations[$key] = $this->context;
}
}
} catch (CodeBaseException $_) {
}
}
/**
* 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 && !UseReturnValuePlugin::$use_dynamic) {
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->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;
}
}
/**
* @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;
}
if ($used && !UseReturnValuePlugin::$use_dynamic) {
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, true, true);
} catch (Exception $_) {
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);
}
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.