mirror of https://github.com/rectorphp/rector.git
204 lines
8.0 KiB
PHP
204 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare (strict_types=1);
|
|
namespace Rector\TypeDeclaration\AlreadyAssignDetector;
|
|
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\Expr\Assign;
|
|
use PhpParser\Node\Expr\PropertyFetch;
|
|
use PhpParser\Node\Identifier;
|
|
use PhpParser\Node\Stmt;
|
|
use PhpParser\Node\Stmt\ClassLike;
|
|
use PhpParser\Node\Stmt\ClassMethod;
|
|
use PhpParser\Node\Stmt\Else_;
|
|
use PhpParser\Node\Stmt\Expression;
|
|
use PhpParser\Node\Stmt\If_;
|
|
use PhpParser\NodeFinder;
|
|
use PhpParser\NodeTraverser;
|
|
use PHPStan\Type\ObjectType;
|
|
use Rector\NodeAnalyzer\PropertyFetchAnalyzer;
|
|
use Rector\NodeTypeResolver\NodeTypeResolver;
|
|
use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser;
|
|
use Rector\PhpParser\Comparing\NodeComparator;
|
|
use Rector\TypeDeclaration\Matcher\PropertyAssignMatcher;
|
|
use Rector\TypeDeclaration\NodeAnalyzer\AutowiredClassMethodOrPropertyAnalyzer;
|
|
use Rector\ValueObject\MethodName;
|
|
final class ConstructorAssignDetector
|
|
{
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\NodeTypeResolver\NodeTypeResolver
|
|
*/
|
|
private $nodeTypeResolver;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\TypeDeclaration\Matcher\PropertyAssignMatcher
|
|
*/
|
|
private $propertyAssignMatcher;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser
|
|
*/
|
|
private $simpleCallableNodeTraverser;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\TypeDeclaration\NodeAnalyzer\AutowiredClassMethodOrPropertyAnalyzer
|
|
*/
|
|
private $autowiredClassMethodOrPropertyAnalyzer;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\NodeAnalyzer\PropertyFetchAnalyzer
|
|
*/
|
|
private $propertyFetchAnalyzer;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\PhpParser\Comparing\NodeComparator
|
|
*/
|
|
private $nodeComparator;
|
|
/**
|
|
* @var string
|
|
*/
|
|
private const IS_FIRST_LEVEL_STATEMENT = 'first_level_stmt';
|
|
public function __construct(NodeTypeResolver $nodeTypeResolver, PropertyAssignMatcher $propertyAssignMatcher, SimpleCallableNodeTraverser $simpleCallableNodeTraverser, AutowiredClassMethodOrPropertyAnalyzer $autowiredClassMethodOrPropertyAnalyzer, PropertyFetchAnalyzer $propertyFetchAnalyzer, NodeComparator $nodeComparator)
|
|
{
|
|
$this->nodeTypeResolver = $nodeTypeResolver;
|
|
$this->propertyAssignMatcher = $propertyAssignMatcher;
|
|
$this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser;
|
|
$this->autowiredClassMethodOrPropertyAnalyzer = $autowiredClassMethodOrPropertyAnalyzer;
|
|
$this->propertyFetchAnalyzer = $propertyFetchAnalyzer;
|
|
$this->nodeComparator = $nodeComparator;
|
|
}
|
|
public function isPropertyAssigned(ClassLike $classLike, string $propertyName) : bool
|
|
{
|
|
$initializeClassMethods = $this->matchInitializeClassMethod($classLike);
|
|
if ($initializeClassMethods === []) {
|
|
return \false;
|
|
}
|
|
$isAssignedInConstructor = \false;
|
|
$this->decorateFirstLevelStatementAttribute($initializeClassMethods);
|
|
foreach ($initializeClassMethods as $initializeClassMethod) {
|
|
$this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $initializeClassMethod->stmts, function (Node $node) use($propertyName, &$isAssignedInConstructor) : ?int {
|
|
if ($this->isIfElseAssign($node, $propertyName)) {
|
|
$isAssignedInConstructor = \true;
|
|
return NodeTraverser::STOP_TRAVERSAL;
|
|
}
|
|
$expr = $this->matchAssignExprToPropertyName($node, $propertyName);
|
|
if (!$expr instanceof Expr) {
|
|
return null;
|
|
}
|
|
/** @var Assign $assign */
|
|
$assign = $node;
|
|
// is merged in assign?
|
|
if ($this->isPropertyUsedInAssign($assign, $propertyName)) {
|
|
$isAssignedInConstructor = \false;
|
|
return NodeTraverser::STOP_TRAVERSAL;
|
|
}
|
|
$isFirstLevelStatement = $assign->getAttribute(self::IS_FIRST_LEVEL_STATEMENT);
|
|
// cannot be nested
|
|
if ($isFirstLevelStatement !== \true) {
|
|
return null;
|
|
}
|
|
$isAssignedInConstructor = \true;
|
|
return NodeTraverser::STOP_TRAVERSAL;
|
|
});
|
|
}
|
|
if (!$isAssignedInConstructor) {
|
|
return $this->propertyFetchAnalyzer->isFilledViaMethodCallInConstructStmts($classLike, $propertyName);
|
|
}
|
|
return $isAssignedInConstructor;
|
|
}
|
|
/**
|
|
* @param Stmt[] $stmts
|
|
*/
|
|
private function isAssignedInStmts(array $stmts, string $propertyName) : bool
|
|
{
|
|
$isAssigned = \false;
|
|
foreach ($stmts as $stmt) {
|
|
// non Expression can be on next stmt
|
|
if (!$stmt instanceof Expression) {
|
|
$isAssigned = \false;
|
|
break;
|
|
}
|
|
if ($this->matchAssignExprToPropertyName($stmt->expr, $propertyName) instanceof Expr) {
|
|
$isAssigned = \true;
|
|
}
|
|
}
|
|
return $isAssigned;
|
|
}
|
|
private function isIfElseAssign(Node $node, string $propertyName) : bool
|
|
{
|
|
if (!$node instanceof If_ || $node->elseifs !== [] || !$node->else instanceof Else_) {
|
|
return \false;
|
|
}
|
|
return $this->isAssignedInStmts($node->stmts, $propertyName) && $this->isAssignedInStmts($node->else->stmts, $propertyName);
|
|
}
|
|
private function matchAssignExprToPropertyName(Node $node, string $propertyName) : ?Expr
|
|
{
|
|
if (!$node instanceof Assign) {
|
|
return null;
|
|
}
|
|
return $this->propertyAssignMatcher->matchPropertyAssignExpr($node, $propertyName);
|
|
}
|
|
/**
|
|
* @param ClassMethod[] $classMethods
|
|
*/
|
|
private function decorateFirstLevelStatementAttribute(array $classMethods) : void
|
|
{
|
|
foreach ($classMethods as $classMethod) {
|
|
foreach ((array) $classMethod->stmts as $methodStmt) {
|
|
$methodStmt->setAttribute(self::IS_FIRST_LEVEL_STATEMENT, \true);
|
|
if ($methodStmt instanceof Expression) {
|
|
$methodStmt->expr->setAttribute(self::IS_FIRST_LEVEL_STATEMENT, \true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* @return ClassMethod[]
|
|
*/
|
|
private function matchInitializeClassMethod(ClassLike $classLike) : array
|
|
{
|
|
$initializingClassMethods = [];
|
|
$constructClassMethod = $classLike->getMethod(MethodName::CONSTRUCT);
|
|
if ($constructClassMethod instanceof ClassMethod) {
|
|
$initializingClassMethods[] = $constructClassMethod;
|
|
}
|
|
$testCaseObjectType = new ObjectType('PHPUnit\\Framework\\TestCase');
|
|
if ($this->nodeTypeResolver->isObjectType($classLike, $testCaseObjectType)) {
|
|
$setUpClassMethod = $classLike->getMethod(MethodName::SET_UP);
|
|
if ($setUpClassMethod instanceof ClassMethod) {
|
|
$initializingClassMethods[] = $setUpClassMethod;
|
|
}
|
|
$setUpBeforeClassMethod = $classLike->getMethod(MethodName::SET_UP_BEFORE_CLASS);
|
|
if ($setUpBeforeClassMethod instanceof ClassMethod) {
|
|
$initializingClassMethods[] = $setUpBeforeClassMethod;
|
|
}
|
|
}
|
|
foreach ($classLike->getMethods() as $classMethod) {
|
|
if (!$this->autowiredClassMethodOrPropertyAnalyzer->detect($classMethod)) {
|
|
continue;
|
|
}
|
|
$initializingClassMethods[] = $classMethod;
|
|
}
|
|
return $initializingClassMethods;
|
|
}
|
|
private function isPropertyUsedInAssign(Assign $assign, string $propertyName) : bool
|
|
{
|
|
$nodeFinder = new NodeFinder();
|
|
$var = $assign->var;
|
|
return (bool) $nodeFinder->findFirst($assign->expr, function (Node $node) use($propertyName, $var) : ?bool {
|
|
if (!$node instanceof PropertyFetch) {
|
|
return null;
|
|
}
|
|
if (!$node->name instanceof Identifier) {
|
|
return null;
|
|
}
|
|
if ($node->name->toString() !== $propertyName) {
|
|
return null;
|
|
}
|
|
return $this->nodeComparator->areNodesEqual($node, $var);
|
|
});
|
|
}
|
|
}
|