rector/src/PhpParser/Node/BetterNodeFinder.php

642 lines
23 KiB
PHP

<?php
declare (strict_types=1);
namespace Rector\Core\PhpParser\Node;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\Switch_;
use PhpParser\Node\Stmt\While_;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use Rector\Core\Contract\PhpParser\Node\StmtsAwareInterface;
use Rector\Core\Exception\StopSearchException;
use Rector\Core\NodeAnalyzer\ClassAnalyzer;
use Rector\Core\PhpParser\Comparing\NodeComparator;
use Rector\Core\PhpParser\Node\CustomNode\FileWithoutNamespace;
use Rector\Core\Provider\CurrentFileProvider;
use Rector\Core\Util\MultiInstanceofChecker;
use Rector\Core\ValueObject\Application\File;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser;
use RectorPrefix202306\Webmozart\Assert\Assert;
/**
* @see \Rector\Core\Tests\PhpParser\Node\BetterNodeFinder\BetterNodeFinderTest
*/
final class BetterNodeFinder
{
/**
* @readonly
* @var \PhpParser\NodeFinder
*/
private $nodeFinder;
/**
* @readonly
* @var \Rector\NodeNameResolver\NodeNameResolver
*/
private $nodeNameResolver;
/**
* @readonly
* @var \Rector\Core\PhpParser\Comparing\NodeComparator
*/
private $nodeComparator;
/**
* @readonly
* @var \Rector\Core\NodeAnalyzer\ClassAnalyzer
*/
private $classAnalyzer;
/**
* @readonly
* @var \Rector\Core\Util\MultiInstanceofChecker
*/
private $multiInstanceofChecker;
/**
* @readonly
* @var \Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser
*/
private $simpleCallableNodeTraverser;
/**
* @readonly
* @var \Rector\Core\Provider\CurrentFileProvider
*/
private $currentFileProvider;
public function __construct(NodeFinder $nodeFinder, NodeNameResolver $nodeNameResolver, NodeComparator $nodeComparator, ClassAnalyzer $classAnalyzer, MultiInstanceofChecker $multiInstanceofChecker, SimpleCallableNodeTraverser $simpleCallableNodeTraverser, CurrentFileProvider $currentFileProvider)
{
$this->nodeFinder = $nodeFinder;
$this->nodeNameResolver = $nodeNameResolver;
$this->nodeComparator = $nodeComparator;
$this->classAnalyzer = $classAnalyzer;
$this->multiInstanceofChecker = $multiInstanceofChecker;
$this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser;
$this->currentFileProvider = $currentFileProvider;
}
/**
* @template TNode of \PhpParser\Node
* @param array<class-string<TNode>> $types
* @return TNode|null
*/
public function findParentByTypes(Node $node, array $types) : ?Node
{
Assert::allIsAOf($types, Node::class);
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
while ($parentNode instanceof Node) {
foreach ($types as $type) {
if ($parentNode instanceof $type) {
return $parentNode;
}
}
$parentNode = $parentNode->getAttribute(AttributeKey::PARENT_NODE);
}
return null;
}
/**
* @template T of Node
* @param class-string<T> $type
* @return T|null
*/
public function findParentType(Node $node, string $type) : ?Node
{
Assert::isAOf($type, Node::class);
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
while ($parentNode instanceof Node) {
if ($parentNode instanceof $type) {
return $parentNode;
}
$parentNode = $parentNode->getAttribute(AttributeKey::PARENT_NODE);
}
return null;
}
/**
* @template T of Node
* @param array<class-string<T>> $types
* @param \PhpParser\Node|mixed[] $nodes
* @return T[]
*/
public function findInstancesOf($nodes, array $types) : array
{
$foundInstances = [];
foreach ($types as $type) {
$currentFoundInstances = $this->findInstanceOf($nodes, $type);
$foundInstances = \array_merge($foundInstances, $currentFoundInstances);
}
return $foundInstances;
}
/**
* @template T of Node
* @param class-string<T> $type
* @param \PhpParser\Node|mixed[] $nodes
* @return T[]
*/
public function findInstanceOf($nodes, string $type) : array
{
return $this->nodeFinder->findInstanceOf($nodes, $type);
}
/**
* @template T of Node
* @param class-string<T> $type
* @return T|null
*
* @param \PhpParser\Node|mixed[] $nodes
*/
public function findFirstInstanceOf($nodes, string $type) : ?Node
{
Assert::isAOf($type, Node::class);
return $this->nodeFinder->findFirstInstanceOf($nodes, $type);
}
/**
* @param class-string<Node> $type
* @param Node[] $nodes
*/
public function hasInstanceOfName(array $nodes, string $type, string $name) : bool
{
Assert::isAOf($type, Node::class);
return (bool) $this->findInstanceOfName($nodes, $type, $name);
}
/**
* @param Node[] $nodes
*/
public function hasVariableOfName(array $nodes, string $name) : bool
{
return $this->findVariableOfName($nodes, $name) instanceof Node;
}
/**
* @api
* @param \PhpParser\Node|mixed[] $nodes
* @return Variable|null
*/
public function findVariableOfName($nodes, string $name) : ?Node
{
return $this->findInstanceOfName($nodes, Variable::class, $name);
}
/**
* @param \PhpParser\Node|mixed[] $nodes
* @param array<class-string<Node>> $types
*/
public function hasInstancesOf($nodes, array $types) : bool
{
Assert::allIsAOf($types, Node::class);
foreach ($types as $type) {
$foundNode = $this->nodeFinder->findFirstInstanceOf($nodes, $type);
if (!$foundNode instanceof Node) {
continue;
}
return \true;
}
return \false;
}
/**
* @api used in Symfony
* @template T of Node
*
* @param Stmt[] $nodes
* @param class-string<T> $type
*/
public function findLastInstanceOf(array $nodes, string $type) : ?Node
{
Assert::allIsAOf($nodes, Stmt::class);
Assert::isAOf($type, Node::class);
$foundInstances = $this->nodeFinder->findInstanceOf($nodes, $type);
if ($foundInstances === []) {
return null;
}
\end($foundInstances);
$lastItemKey = \key($foundInstances);
return $foundInstances[$lastItemKey];
}
/**
* @param \PhpParser\Node|mixed[] $nodes
* @param callable(Node $node): bool $filter
* @return Node[]
*/
public function find($nodes, callable $filter) : array
{
return $this->nodeFinder->find($nodes, $filter);
}
/**
* @api symfony
* @param Node[] $nodes
* @return ClassLike|null
*/
public function findFirstNonAnonymousClass(array $nodes) : ?Node
{
// skip anonymous classes
return $this->findFirst($nodes, function (Node $node) : bool {
return $node instanceof Class_ && !$this->classAnalyzer->isAnonymousClass($node);
});
}
/**
* @param \PhpParser\Node|mixed[] $nodes
* @param callable(Node $filter): bool $filter
*/
public function findFirst($nodes, callable $filter) : ?Node
{
return $this->nodeFinder->findFirst($nodes, $filter);
}
/**
* @return Assign[]
*/
public function findClassMethodAssignsToLocalProperty(ClassMethod $classMethod, string $propertyName) : array
{
/** @var Assign[] $assigns */
$assigns = [];
$this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $classMethod->stmts, function (Node $node) use($propertyName, &$assigns) {
// skip anonymous classes and inner function
if ($node instanceof Class_ || $node instanceof Function_) {
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
if (!$node instanceof Assign) {
return null;
}
if (!$node->var instanceof PropertyFetch) {
return null;
}
$propertyFetch = $node->var;
if (!$this->nodeNameResolver->isName($propertyFetch->var, 'this')) {
return null;
}
if (!$this->nodeNameResolver->isName($propertyFetch->name, $propertyName)) {
return null;
}
$assigns[] = $node;
return $node;
});
return $assigns;
}
/**
* @api symfony
* @return Assign|null
*/
public function findPreviousAssignToExpr(Expr $expr) : ?Node
{
return $this->findFirstPrevious($expr, function (Node $node) use($expr) : bool {
if (!$node instanceof Assign) {
return \false;
}
return $this->nodeComparator->areNodesEqual($node->var, $expr);
});
}
/**
* Search in previous Node/Stmt, when no Node found, lookup previous Stmt of Parent Node
*
* @param callable(Node $node): bool $filter
*/
public function findFirstPrevious(Node $node, callable $filter) : ?Node
{
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
$newStmts = $this->resolveNewStmts($parentNode);
$foundNode = $this->findFirstInlinedPrevious($node, $filter, $newStmts, $parentNode);
// we found what we need
if ($foundNode instanceof Node) {
return $foundNode;
}
if ($parentNode instanceof FunctionLike) {
return null;
}
if ($parentNode instanceof Node) {
return $this->findFirstPrevious($parentNode, $filter);
}
return null;
}
/**
* @api
* @template T of Node
*
* @param array<class-string<T>> $types
* @return T|null
*/
public function findFirstPreviousOfTypes(Node $mainNode, array $types) : ?Node
{
return $this->findFirstPrevious($mainNode, function (Node $node) use($types) : bool {
return $this->multiInstanceofChecker->isInstanceOf($node, $types);
});
}
/**
* @param callable(Node $node): bool $filter
*/
public function findFirstNext(Node $node, callable $filter) : ?Node
{
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
$newStmts = $this->resolveNewStmts($parentNode);
try {
$foundNode = $this->findFirstInlinedNext($node, $filter, $newStmts, $parentNode);
} catch (StopSearchException $exception) {
return null;
}
// we found what we need
if ($foundNode instanceof Node) {
return $foundNode;
}
if ($parentNode instanceof Return_ || $parentNode instanceof FunctionLike) {
return null;
}
if ($parentNode instanceof Node) {
return $this->findFirstNext($parentNode, $filter);
}
return null;
}
/**
* @template T of Node
* @param array<class-string<T>>|class-string<T> $types
* @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike
*/
public function hasInstancesOfInFunctionLikeScoped($functionLike, $types) : bool
{
if (\is_string($types)) {
$types = [$types];
}
foreach ($types as $type) {
$foundNodes = $this->findInstanceOf((array) $functionLike->stmts, $type);
foreach ($foundNodes as $foundNode) {
$parentFunctionLike = $this->findParentByTypes($foundNode, [ClassMethod::class, Function_::class, Closure::class]);
if ($parentFunctionLike === $functionLike) {
return \true;
}
}
}
return \false;
}
/**
* @template T of Node
* @param array<class-string<T>>|class-string<T> $types
* @return T[]
* @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike
*/
public function findInstancesOfInFunctionLikeScoped($functionLike, $types) : array
{
if (\is_string($types)) {
$types = [$types];
}
/** @var T[] $foundNodes */
$foundNodes = [];
foreach ($types as $type) {
/** @var T[] $nodes */
$nodes = $this->findInstanceOf((array) $functionLike->stmts, $type);
if ($nodes === []) {
continue;
}
foreach ($nodes as $key => $node) {
$parentFunctionLike = $this->findParentByTypes($node, [ClassMethod::class, Function_::class, Closure::class]);
if ($parentFunctionLike !== $functionLike) {
unset($nodes[$key]);
}
}
if ($nodes === []) {
continue;
}
$foundNodes = \array_merge($foundNodes, $nodes);
}
return $foundNodes;
}
/**
* @param callable(Node $node): bool $filter
* @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike
*/
public function findFirstInFunctionLikeScoped($functionLike, callable $filter) : ?Node
{
$foundNode = $this->findFirst((array) $functionLike->stmts, $filter);
if (!$foundNode instanceof Node) {
return null;
}
$parentFunctionLike = $this->findParentByTypes($foundNode, [ClassMethod::class, Function_::class, Closure::class, Class_::class]);
if ($parentFunctionLike !== $functionLike) {
return null;
}
return $foundNode;
}
public function resolveCurrentStatement(Node $node) : ?Stmt
{
if ($node instanceof Stmt) {
return $node;
}
$currentStmt = $node;
while (($currentStmt = $currentStmt->getAttribute(AttributeKey::PARENT_NODE)) instanceof Node) {
if ($currentStmt instanceof Stmt) {
return $currentStmt;
}
/** @var Node|null $currentStmt */
if (!$currentStmt instanceof Node) {
return null;
}
}
return null;
}
/**
* @api
*
* Resolve previous node from any Node, eg: Expr, Identifier, Name, etc
*/
public function resolvePreviousNode(Node $node) : ?Node
{
$currentStmt = $this->resolveCurrentStatement($node);
if (!$currentStmt instanceof Stmt) {
return null;
}
$startTokenPos = $node->getStartTokenPos();
$nodes = $startTokenPos < 0 || $currentStmt->getStartTokenPos() === $startTokenPos ? [] : $this->find($currentStmt, static function (Node $subNode) use($startTokenPos) : bool {
return $subNode->getEndTokenPos() < $startTokenPos;
});
if ($nodes === []) {
$parentNode = $currentStmt->getAttribute(AttributeKey::PARENT_NODE);
if (!$this->isAllowedParentNode($parentNode)) {
return null;
}
$currentStmtKey = $currentStmt->getAttribute(AttributeKey::STMT_KEY);
/** @var StmtsAwareInterface|ClassLike|Declare_ $parentNode */
return $parentNode->stmts[$currentStmtKey - 1] ?? null;
}
return \end($nodes);
}
/**
* @api
*
* Resolve next node from any Node, eg: Expr, Identifier, Name, etc
*/
public function resolveNextNode(Node $node) : ?Node
{
$currentStmt = $this->resolveCurrentStatement($node);
if (!$currentStmt instanceof Stmt) {
return null;
}
$endTokenPos = $node->getEndTokenPos();
$nextNode = $endTokenPos < 0 || $currentStmt->getEndTokenPos() === $endTokenPos ? null : $this->findFirst($currentStmt, static function (Node $subNode) use($endTokenPos) : bool {
return $subNode->getStartTokenPos() > $endTokenPos;
});
if (!$nextNode instanceof Node) {
$parentNode = $currentStmt->getAttribute(AttributeKey::PARENT_NODE);
if (!$this->isAllowedParentNode($parentNode)) {
return null;
}
$currentStmtKey = $currentStmt->getAttribute(AttributeKey::STMT_KEY);
/** @var StmtsAwareInterface|ClassLike|Declare_ $parentNode */
return $parentNode->stmts[$currentStmtKey + 1] ?? null;
}
return $nextNode;
}
private function isAllowedParentNode(?Node $node) : bool
{
return $node instanceof StmtsAwareInterface || $node instanceof ClassLike || $node instanceof Declare_;
}
/**
* Only search in next Node/Stmt
*
* @param Stmt[] $newStmts
* @param callable(Node $node): bool $filter
*/
private function findFirstInlinedNext(Node $node, callable $filter, array $newStmts, ?Node $parentNode) : ?Node
{
if (!$parentNode instanceof Node) {
$nextNode = $this->resolveNextNodeFromFile($newStmts, $node);
} elseif ($node instanceof Stmt) {
if (!$this->isAllowedParentNode($parentNode)) {
return null;
}
$currentStmtKey = $node->getAttribute(AttributeKey::STMT_KEY);
/** @var StmtsAwareInterface|ClassLike|Declare_ $parentNode */
$nextNode = $parentNode->stmts[$currentStmtKey + 1] ?? null;
} else {
$nextNode = $this->resolveNextNode($node);
}
if (!$nextNode instanceof Node) {
return null;
}
if ($nextNode instanceof Return_ && !$nextNode->expr instanceof Expr && !$parentNode instanceof Case_) {
throw new StopSearchException();
}
$found = $this->findFirst($nextNode, $filter);
if ($found instanceof Node) {
return $found;
}
return $this->findFirstInlinedNext($nextNode, $filter, $newStmts, $parentNode);
}
/**
* @return Stmt[]
*/
private function resolveNewStmts(?Node $parentNode) : array
{
if (!$parentNode instanceof Node) {
// on __construct(), $file not yet a File object
$file = $this->currentFileProvider->getFile();
return $file instanceof File ? $file->getNewStmts() : [];
}
return [];
}
/**
* @param callable(Node $node): bool $filter
*/
private function findFirstInTopLevelStmtsAware(StmtsAwareInterface $stmtsAware, callable $filter) : ?Node
{
$nodes = [];
if ($stmtsAware instanceof Foreach_) {
$nodes = [$stmtsAware->valueVar, $stmtsAware->keyVar, $stmtsAware->expr];
}
if ($stmtsAware instanceof For_) {
$nodes = [$stmtsAware->loop, $stmtsAware->cond, $stmtsAware->init];
}
if ($this->multiInstanceofChecker->isInstanceOf($stmtsAware, [If_::class, While_::class, Do_::class, Switch_::class, ElseIf_::class, Case_::class])) {
/** @var If_|While_|Do_|Switch_|ElseIf_|Case_ $stmtsAware */
$nodes = [$stmtsAware->cond];
}
foreach ($nodes as $node) {
if (!$node instanceof Node) {
continue;
}
$foundNode = $this->findFirst($node, $filter);
if ($foundNode instanceof Node) {
return $foundNode;
}
}
return null;
}
/**
* @param Stmt[] $newStmts
*/
private function resolvePreviousNodeFromFile(array $newStmts, Node $node) : ?Node
{
if (!$node instanceof Namespace_ && !$node instanceof FileWithoutNamespace) {
return null;
}
$currentStmtKey = $node->getAttribute(AttributeKey::STMT_KEY);
$stmtKey = $currentStmtKey - 1;
if ($node instanceof FileWithoutNamespace) {
$stmtKey = $stmtKey === -1 ? 0 : $stmtKey;
}
return $newStmts[$stmtKey] ?? null;
}
/**
* @param Stmt[] $newStmts
*/
private function resolveNextNodeFromFile(array $newStmts, Node $node) : ?Node
{
if (!$node instanceof Namespace_ && !$node instanceof FileWithoutNamespace) {
return null;
}
$currentStmtKey = $node->getAttribute(AttributeKey::STMT_KEY);
return $newStmts[$currentStmtKey + 1] ?? null;
}
/**
* Only search in previous Node/Stmt
*
* @param Stmt[] $newStmts
* @param callable(Node $node): bool $filter
*/
private function findFirstInlinedPrevious(Node $node, callable $filter, array $newStmts, ?Node $parentNode) : ?Node
{
if (!$parentNode instanceof Node) {
$previousNode = $this->resolvePreviousNodeFromFile($newStmts, $node);
} elseif ($node instanceof Stmt) {
if (!$this->isAllowedParentNode($parentNode)) {
return null;
}
$currentStmtKey = $node->getAttribute(AttributeKey::STMT_KEY);
if ($parentNode instanceof StmtsAwareInterface && !isset($parentNode->stmts[$currentStmtKey - 1])) {
return $this->findFirstInTopLevelStmtsAware($parentNode, $filter);
}
/** @var StmtsAwareInterface|ClassLike|Declare_ $parentNode */
$previousNode = $parentNode->stmts[$currentStmtKey - 1] ?? null;
} else {
$previousNode = $this->resolvePreviousNode($node);
}
if (!$previousNode instanceof Node) {
return null;
}
$foundNode = $this->findFirst($previousNode, $filter);
// we found what we need
if ($foundNode instanceof Node) {
return $foundNode;
}
return $this->findFirstInlinedPrevious($previousNode, $filter, $newStmts, $parentNode);
}
/**
* @template T of Node
* @param \PhpParser\Node|mixed[] $nodes
* @param class-string<T> $type
*/
private function findInstanceOfName($nodes, string $type, string $name) : ?Node
{
Assert::isAOf($type, Node::class);
return $this->nodeFinder->findFirst($nodes, function (Node $node) use($type, $name) : bool {
return $node instanceof $type && $this->nodeNameResolver->isName($node, $name);
});
}
}