rector/src/PhpParser/Node/BetterNodeFinder.php

486 lines
16 KiB
PHP
Raw Normal View History

2019-10-13 05:59:52 +00:00
<?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\StaticPropertyFetch;
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\Function_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeFinder;
use Rector\Core\NodeAnalyzer\ClassAnalyzer;
use Rector\Core\PhpParser\Comparing\NodeComparator;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use RectorPrefix202207\Symplify\PackageBuilder\Php\TypeChecker;
use RectorPrefix202207\Webmozart\Assert\Assert;
2019-09-03 09:11:45 +00:00
/**
* @see \Rector\Core\Tests\PhpParser\Node\BetterNodeFinder\BetterNodeFinderTest
2019-09-03 09:11:45 +00:00
*/
final class BetterNodeFinder
{
/**
* @readonly
* @var \PhpParser\NodeFinder
*/
private $nodeFinder;
/**
* @readonly
* @var \Rector\NodeNameResolver\NodeNameResolver
*/
private $nodeNameResolver;
2020-07-25 20:02:44 +00:00
/**
* @readonly
* @var \Symplify\PackageBuilder\Php\TypeChecker
2020-07-25 20:02:44 +00:00
*/
private $typeChecker;
/**
* @readonly
* @var \Rector\Core\PhpParser\Comparing\NodeComparator
*/
private $nodeComparator;
/**
* @readonly
* @var \Rector\Core\NodeAnalyzer\ClassAnalyzer
*/
private $classAnalyzer;
public function __construct(NodeFinder $nodeFinder, NodeNameResolver $nodeNameResolver, TypeChecker $typeChecker, NodeComparator $nodeComparator, ClassAnalyzer $classAnalyzer)
{
$this->nodeFinder = $nodeFinder;
$this->nodeNameResolver = $nodeNameResolver;
$this->typeChecker = $typeChecker;
$this->nodeComparator = $nodeComparator;
$this->classAnalyzer = $classAnalyzer;
}
/**
* @template T of \PhpParser\Node
* @param array<class-string<T>> $types
* @return T|null
*/
public function findParentByTypes(Node $currentNode, array $types) : ?Node
{
Assert::allIsAOf($types, Node::class);
while ($currentNode = $currentNode->getAttribute(AttributeKey::PARENT_NODE)) {
if (!$currentNode instanceof Node) {
return null;
}
foreach ($types as $type) {
if (\is_a($currentNode, $type, \true)) {
return $currentNode;
}
}
}
return null;
}
/**
2021-03-18 11:21:20 +00:00
* @template T of Node
2021-01-08 22:30:33 +00:00
* @param class-string<T> $type
* @return T|null
*/
public function findParentType(Node $node, string $type) : ?Node
2021-01-08 22:30:33 +00:00
{
Assert::isAOf($type, Node::class);
$parent = $node->getAttribute(AttributeKey::PARENT_NODE);
if (!$parent instanceof Node) {
2021-01-08 22:30:33 +00:00
return null;
}
do {
if (\is_a($parent, $type, \true)) {
2021-01-08 22:30:33 +00:00
return $parent;
}
$parent = $parent->getAttribute(AttributeKey::PARENT_NODE);
} while ($parent instanceof Node);
2021-01-08 22:30:33 +00:00
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;
}
/**
2021-03-18 11:21:20 +00:00
* @template T of Node
2021-01-08 22:30:33 +00:00
* @param class-string<T> $type
* @param \PhpParser\Node|mixed[] $nodes
2021-01-08 22:30:33 +00:00
* @return T[]
*/
public function findInstanceOf($nodes, string $type) : array
{
return $this->nodeFinder->findInstanceOf($nodes, $type);
}
/**
2021-03-18 11:21:20 +00:00
* @template T of Node
2021-01-08 22:30:33 +00:00
* @param class-string<T> $type
* @param \PhpParser\Node|mixed[] $nodes
*/
public function findFirstInstanceOf($nodes, string $type) : ?Node
{
Assert::isAOf($type, Node::class);
return $this->nodeFinder->findFirstInstanceOf($nodes, $type);
}
/**
2021-03-18 11:21:20 +00:00
* @param class-string<Node> $type
* @param \PhpParser\Node|mixed[] $nodes
*/
public function hasInstanceOfName($nodes, string $type, string $name) : bool
2020-07-19 13:39:13 +00:00
{
Assert::isAOf($type, Node::class);
2020-07-19 13:39:13 +00:00
return (bool) $this->findInstanceOfName($nodes, $type, $name);
}
/**
* @param \PhpParser\Node|mixed[] $nodes
2020-07-19 13:39:13 +00:00
*/
public function hasVariableOfName($nodes, string $name) : bool
2020-07-19 13:39:13 +00:00
{
return $this->findVariableOfName($nodes, $name) instanceof Node;
2020-07-19 13:39:13 +00:00
}
/**
* @param \PhpParser\Node|mixed[] $nodes
2021-01-08 22:30:33 +00:00
* @return Variable|null
2020-07-19 13:39:13 +00:00
*/
public function findVariableOfName($nodes, string $name) : ?Node
{
return $this->findInstanceOfName($nodes, Variable::class, $name);
}
/**
* @param \PhpParser\Node|mixed[] $nodes
2021-03-18 11:21:20 +00:00
* @param array<class-string<Node>> $types
*/
public function hasInstancesOf($nodes, array $types) : bool
{
Assert::allIsAOf($types, Node::class);
foreach ($types as $type) {
2021-03-18 11:21:20 +00:00
$foundNode = $this->nodeFinder->findFirstInstanceOf($nodes, $type);
if (!$foundNode instanceof Node) {
continue;
}
return \true;
}
return \false;
}
/**
2021-03-18 11:21:20 +00:00
* @template T of Node
2021-01-08 22:30:33 +00:00
* @param class-string<T> $type
* @param \PhpParser\Node|mixed[] $nodes
*/
public function findLastInstanceOf($nodes, string $type) : ?Node
{
Assert::isAOf($type, Node::class);
$foundInstances = $this->nodeFinder->findInstanceOf($nodes, $type);
2019-02-17 14:12:47 +00:00
if ($foundInstances === []) {
return null;
}
\end($foundInstances);
$lastItemKey = \key($foundInstances);
2021-01-08 22:30:33 +00:00
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);
}
2020-07-05 11:07:20 +00:00
/**
* @param Node[] $nodes
2021-01-08 22:30:33 +00:00
* @return ClassLike|null
2020-07-05 11:07:20 +00:00
*/
public function findFirstNonAnonymousClass(array $nodes) : ?Node
2020-07-05 11:07:20 +00:00
{
// skip anonymous classes
return $this->findFirst($nodes, function (Node $node) : bool {
return $node instanceof Class_ && !$this->classAnalyzer->isAnonymousClass($node);
2020-07-05 11:07:20 +00:00
});
}
2017-11-01 21:37:57 +00:00
/**
* @param \PhpParser\Node|mixed[] $nodes
Updated Rector to commit 1dc94831c504b3f99936f12d140401aeb10835d2 https://github.com/rectorphp/rector-src/commit/704b1bec116787abf4d48b1b2193b871dae57a13 [DowngradePhp80] Add DowngradeNumberFormatNoFourthArgRector https://github.com/rectorphp/rector-src/commit/37e2e845001a8740437ab911aca0576aa66c6eed [ci-review] Rector Rectify https://github.com/rectorphp/rector-src/commit/c6f84fa033a499c9ae74d324c004e15c4353e91a [ci-review] Rector Rectify https://github.com/rectorphp/rector-src/commit/31b5cbf4b299fa7a4a113a8be15a7df4a59bc4ac skip named arg https://github.com/rectorphp/rector-src/commit/490dbb63655677f63008f0f1e002a1e4b557182e skip different func call https://github.com/rectorphp/rector-src/commit/05854d69d03de7a4b6da504f787a84c79cc67258 skip has fourth arg https://github.com/rectorphp/rector-src/commit/d7bf81e064ae240e08af908d13091f7a02be6a71 skip no third arg https://github.com/rectorphp/rector-src/commit/8f18542a0087763600708ed586b2d1780e711c0b add failing test case https://github.com/rectorphp/rector-src/commit/7df7bcae202d3af50501cdac12ace9bb04b9d8bb implemented https://github.com/rectorphp/rector-src/commit/7702d5df1f790400fbf337ecba9735846a020d01 register https://github.com/rectorphp/rector-src/commit/efcd01a9614d497576d8f136aacbb455f3c77d07 phpstan https://github.com/rectorphp/rector-src/commit/6f7b8a2788452c6fefd97b30dea08e843782e9bb phpstan https://github.com/rectorphp/rector-src/commit/d54672face49f81d42435d00693734edabbeb3a5 phpstan https://github.com/rectorphp/rector-src/commit/2fd995c3967704babf0d1d3a24dd2a2ed7e573fd [ci-review] Rector Rectify https://github.com/rectorphp/rector-src/commit/3c2abcb79dc7fa167e4956c0f1cf355dd0cf605e [ci-review] Rector Rectify https://github.com/rectorphp/rector-src/commit/2b16ac97610cfa1d70f0fc89f415ab380e7756a5 phpstan https://github.com/rectorphp/rector-src/commit/58f3574328d8bbdfcd5cf1050b3ae35f835accf0 phpstan https://github.com/rectorphp/rector-src/commit/620d2b312559b02e0c5fd6bd8f3e1019cc1a2b7d [ci-review] Rector Rectify https://github.com/rectorphp/rector-src/commit/ec364cbd0330c25c684d7457fe93bdfe09ae2956 [ci-review] Rector Rectify https://github.com/rectorphp/rector-src/commit/0355abcde2d4bd7cca6f2a09c35ef3a741a9542b rectify fix https://github.com/rectorphp/rector-src/commit/1dc94831c504b3f99936f12d140401aeb10835d2 Merge pull request #1649 from rectorphp/downgrade-php80-numberformat
2022-01-08 17:02:43 +00:00
* @param callable(Node $filter): bool $filter
2017-11-01 21:37:57 +00:00
*/
public function findFirst($nodes, callable $filter) : ?Node
2017-11-01 21:37:57 +00:00
{
return $this->nodeFinder->findFirst($nodes, $filter);
}
/**
* @return Assign[]
*/
public function findClassMethodAssignsToLocalProperty(ClassMethod $classMethod, string $propertyName) : array
{
return $this->find((array) $classMethod->stmts, function (Node $node) use($classMethod, $propertyName) : bool {
if (!$node instanceof Assign) {
return \false;
}
if (!$node->var instanceof PropertyFetch) {
return \false;
}
$propertyFetch = $node->var;
if (!$this->nodeNameResolver->isName($propertyFetch->var, 'this')) {
return \false;
}
$parentFunctionLike = $this->findParentType($node, ClassMethod::class);
if ($parentFunctionLike !== $classMethod) {
return \false;
}
return $this->nodeNameResolver->isName($propertyFetch->name, $propertyName);
});
}
2021-01-08 22:30:33 +00:00
/**
* @return Assign|null
*/
public function findPreviousAssignToExpr(Expr $expr) : ?Node
2020-07-25 20:02:44 +00:00
{
return $this->findFirstPrevious($expr, function (Node $node) use($expr) : bool {
if (!$node instanceof Assign) {
return \false;
2020-07-25 20:02:44 +00:00
}
return $this->nodeComparator->areNodesEqual($node->var, $expr);
2020-07-25 20:02:44 +00:00
});
}
/**
* Only search in previous Node/Stmt
*
* @param callable(Node $node): bool $filter
*/
public function findFirstInlinedPrevious(Node $node, callable $filter) : ?Node
{
$previousNode = $node->getAttribute(AttributeKey::PREVIOUS_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);
}
/**
* 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
{
$foundNode = $this->findFirstInlinedPrevious($node, $filter);
// we found what we need
if ($foundNode instanceof Node) {
return $foundNode;
}
$parent = $node->getAttribute(AttributeKey::PARENT_NODE);
if ($parent instanceof FunctionLike) {
return null;
}
if ($parent instanceof Node) {
return $this->findFirstPrevious($parent, $filter);
}
return null;
2018-10-09 10:18:42 +00:00
}
2020-03-23 16:13:04 +00:00
/**
2021-03-18 11:21:20 +00:00
* @template T of Node
* @param array<class-string<T>> $types
2020-03-23 16:13:04 +00:00
*/
public function findFirstPreviousOfTypes(Node $mainNode, array $types) : ?Node
2020-03-23 16:13:04 +00:00
{
return $this->findFirstPrevious($mainNode, function (Node $node) use($types) : bool {
return $this->typeChecker->isInstanceOf($node, $types);
});
2020-03-23 16:13:04 +00:00
}
/**
* @param callable(Node $node): bool $filter
*/
public function findFirstNext(Node $node, callable $filter) : ?Node
{
$next = $node->getAttribute(AttributeKey::NEXT_NODE);
if ($next instanceof Node) {
if ($next instanceof Return_ && $next->expr === null) {
$parent = $node->getAttribute(AttributeKey::PARENT_NODE);
if (!$parent instanceof Case_) {
return null;
}
}
$found = $this->findFirst($next, $filter);
if ($found instanceof Node) {
return $found;
}
return $this->findFirstNext($next, $filter);
}
$parent = $node->getAttribute(AttributeKey::PARENT_NODE);
if ($parent instanceof Return_ || $parent instanceof FunctionLike) {
return null;
}
if ($parent instanceof Node) {
return $this->findFirstNext($parent, $filter);
}
return null;
}
/**
* @return Expr[]
* @param \PhpParser\Node\Expr|\PhpParser\Node\Expr\Variable|\PhpParser\Node\Stmt\Property|\PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $expr
*/
public function findSameNamedExprs($expr) : array
{
// assign of empty string to something
$scopeNode = $this->findParentScope($expr);
if (!$scopeNode instanceof Node) {
return [];
}
if ($expr instanceof Variable) {
$exprName = $this->nodeNameResolver->getName($expr);
if ($exprName === null) {
return [];
}
$variables = $this->findInstancesOf($scopeNode, [Variable::class]);
return \array_filter($variables, function (Variable $variable) use($exprName) : bool {
return $this->nodeNameResolver->isName($variable, $exprName);
});
}
if ($expr instanceof Property) {
$singleProperty = $expr->props[0];
$exprName = $this->nodeNameResolver->getName($singleProperty->name);
} elseif ($expr instanceof StaticPropertyFetch || $expr instanceof PropertyFetch) {
$exprName = $this->nodeNameResolver->getName($expr->name);
} else {
return [];
}
if ($exprName === null) {
return [];
}
$propertyFetches = $this->findInstancesOf($scopeNode, [PropertyFetch::class, StaticPropertyFetch::class]);
return \array_filter($propertyFetches, function ($propertyFetch) use($exprName) : bool {
return $this->nodeNameResolver->isName($propertyFetch->name, $exprName);
});
}
/**
* @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)) {
if ($currentStmt instanceof Stmt) {
return $currentStmt;
}
if (!$currentStmt instanceof Node) {
return null;
}
}
return null;
}
/**
2021-03-18 11:21:20 +00:00
* @template T of Node
* @param \PhpParser\Node|mixed[] $nodes
2021-01-08 22:30:33 +00:00
* @param class-string<T> $type
*/
private function findInstanceOfName($nodes, string $type, string $name) : ?Node
{
Assert::isAOf($type, Node::class);
$foundInstances = $this->nodeFinder->findInstanceOf($nodes, $type);
foreach ($foundInstances as $foundInstance) {
if (!$this->nodeNameResolver->isName($foundInstance, $name)) {
2021-01-08 22:30:33 +00:00
continue;
}
2021-01-08 22:30:33 +00:00
return $foundInstance;
}
return null;
}
/**
* @return Closure|Function_|ClassMethod|Class_|Namespace_|null
*/
private function findParentScope(Node $node)
{
return $this->findParentByTypes($node, [Closure::class, Function_::class, ClassMethod::class, Class_::class, Namespace_::class]);
}
}