For example, we want to find all the return nodes in the following code. But we want to skip those inside inner scope - anonymous classes, inner functions or closures.
class SomeClass
{
private const LABEL_A = 'A';
public function run()
{
$someClosure = function () {
return 1;
};
if (rand(0, 1)) {
return 'A';
}
return $someClosure() + 2;
}
}
Previously, we could hook into the Return_ node and check if the node is located within the parent node of PhpParser\Node\Expr\Closure;. How can we detect the exact location now?
In our rule, we hook into the top shared node - PhpParser\Node\Stmt\ClassMethod:
use PhpParser\Node\Stmt\ClassMethod;
// ...
public function getNodeTypes()
{
return [ClassMethod::class];
}
We have 2 ways to skip the closure.
1. Scoped Node Finder
Then, you can do this in the refactor() method:
use PhpParser\Node\Stmt\Return_;
public function refactor(Node $node): ?Node
{
$returns = $this->betterNodeFinder->findInstancesOfInFunctionLikeScoped(
$node, Return_::class
);
if ($returns === []) {
return null;
}
// process $returns nodes here ...
}
You're familiar with findInstanceOf(), which returns all the nodes it can find of a specific type.
The findInstancesOfInFunctionLikeScoped() is similar, but smarter - if it enters an anonymous class, inner function, or closure inside, it will skip it.
In the example above, we'll give you only 2 items:
return 'A';
return $someClosure() + 2;
👍
2. Using Node Traversing
Another option is to use SimpleCallableNodeTraverser. E.g., we need to replace:
-return 'A';
+return false;
Here is how we design the Rector rule:
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Name;
use PhpParser\NodeTraverser;
use PhpParser\Node\Stmt\ClassMethod;
/**
* @param ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if ($node->stmts === null) {
return null;
}
$hasChanged = false;
$this->traverseNodesWithCallable(
$node->stmts,
function (Node $subNode) use (&$hasChanged): ?int {
if ($subNode instanceof Class_
|| $subNode instanceof Function_
|| $subNode instanceof Closure
) {
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
if (! $subNode instanceof Return_) {
return null;
}
if (! $subNode->expr instanceof Expr) {
return null;
}
if (! $this->valueResolver->isValue($subNode->expr, 'A')) {
return null;
}
$subNode->expr = new ConstFetch(new Name('false'));
$hasChanged = true;
return null;
}
);
if ($hasChanged) {
return $node;
}
return null;
}
👍
Pick the solution that fits your situation. That's it ;)
Happy coding!