fix RemoveSetterOnlyPropertyAndMethodCallRector race condition

This commit is contained in:
Tomas Votruba 2019-08-10 11:43:34 +02:00
parent 779e082213
commit 4b584b9507
3 changed files with 215 additions and 128 deletions

View File

@ -0,0 +1,165 @@
<?php declare(strict_types=1);
namespace Rector\DeadCode\Analyzer;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use Rector\NodeContainer\ParsedNodesByType;
use Rector\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\PhpParser\Node\Resolver\NameResolver;
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser;
final class SetterOnlyMethodAnalyzer
{
/**
* @var ParsedNodesByType
*/
private $parsedNodesByType;
/**
* @var ClassManipulator
*/
private $classManipulator;
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var CallableNodeTraverser
*/
private $callableNodeTraverser;
/**
* @var string[][][]
*/
private $propertiesAndMethodsToRemoveByType = [];
public function __construct(
ParsedNodesByType $parsedNodesByType,
ClassManipulator $classManipulator,
NameResolver $nameResolver,
CallableNodeTraverser $callableNodeTraverser
) {
$this->parsedNodesByType = $parsedNodesByType;
$this->classManipulator = $classManipulator;
$this->nameResolver = $nameResolver;
$this->callableNodeTraverser = $callableNodeTraverser;
}
/**
* Returns 1. setter only properties + 2. setter only method names by class type
* @return string[][][]
*/
public function provideSetterOnlyPropertiesAndMethodsByType(): array
{
if ($this->propertiesAndMethodsToRemoveByType !== []) {
return $this->propertiesAndMethodsToRemoveByType;
}
foreach ($this->parsedNodesByType->getClasses() as $class) {
$type = $this->nameResolver->getName($class);
// 1. setter only properties by class
$assignOnlyPrivatePropertyNames = $this->classManipulator->getAssignOnlyPrivatePropertyNames($class);
if ($assignOnlyPrivatePropertyNames) {
$this->propertiesAndMethodsToRemoveByType[$type]['properties'] = $assignOnlyPrivatePropertyNames;
}
// 2. setter only methods by class
$setterOnlyMethodNames = $this->resolveSetterOnlyMethodNames($class, $assignOnlyPrivatePropertyNames);
if ($setterOnlyMethodNames) {
$this->propertiesAndMethodsToRemoveByType[$type]['methods'] = $setterOnlyMethodNames;
}
}
return $this->propertiesAndMethodsToRemoveByType;
}
/**
* @param string[] $assignOnlyPrivatePropertyNames
* @return string[]
*/
private function resolveSetterOnlyMethodNames(Class_ $class, array $assignOnlyPrivatePropertyNames): array
{
$methodNamesToBeRemoved = [];
$this->callableNodeTraverser->traverseNodesWithCallable($class, function (Node $node) use (
$assignOnlyPrivatePropertyNames,
&$methodNamesToBeRemoved
): void {
if (! $this->isClassMethodWithSinglePropertyAssignOfNames($node, $assignOnlyPrivatePropertyNames)) {
return;
}
/** @var string $classMethodName */
$classMethodName = $this->nameResolver->getName($node);
$methodNamesToBeRemoved[] = $classMethodName;
});
return $methodNamesToBeRemoved;
}
/**
* Looks for:
*
* public function <someMethod>($value)
* {
* $this->value = $value
* }
*
* @param string[] $propertyNames
*/
private function isClassMethodWithSinglePropertyAssignOfNames(Node $node, array $propertyNames): bool
{
if (! $node instanceof ClassMethod) {
return false;
}
if ($this->nameResolver->isName($node, '__construct')) {
return false;
}
if (count((array) $node->stmts) !== 1) {
return false;
}
if (! $node->stmts[0] instanceof Expression) {
return false;
}
/** @var Expression $onlyExpression */
$onlyExpression = $node->stmts[0];
$onlyStmt = $onlyExpression->expr;
return $this->isPropertyAssignWithPropertyNames($onlyStmt, $propertyNames);
}
/**
* Is: "$this->value = <$value>"
*
* @param string[] $propertyNames
*/
private function isPropertyAssignWithPropertyNames(Node $node, array $propertyNames): bool
{
if (! $node instanceof Assign) {
return false;
}
if (! $node->var instanceof PropertyFetch) {
return false;
}
$propertyFetch = $node->var;
if (! $this->nameResolver->isName($propertyFetch->var, 'this')) {
return false;
}
return $this->nameResolver->isNames($propertyFetch->name, $propertyNames);
}
}

View File

@ -3,13 +3,11 @@
namespace Rector\DeadCode\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use Rector\NodeContainer\ParsedNodesByType;
use Rector\PhpParser\Node\Manipulator\ClassManipulator;
use PhpParser\Node\Stmt\Property;
use Rector\DeadCode\Analyzer\SetterOnlyMethodAnalyzer;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
@ -21,24 +19,13 @@ use Rector\RectorDefinition\RectorDefinition;
final class RemoveSetterOnlyPropertyAndMethodCallRector extends AbstractRector
{
/**
* @var ClassManipulator
* @var SetterOnlyMethodAnalyzer
*/
private $classManipulator;
private $setterOnlyMethodAnalyzer;
/**
* @var ParsedNodesByType
*/
private $parsedNodesByType;
/**
* @var string[]
*/
private $methodCallNamesToBeRemoved = [];
public function __construct(ClassManipulator $classManipulator, ParsedNodesByType $parsedNodesByType)
public function __construct(SetterOnlyMethodAnalyzer $setterOnlyMethodAnalyzer)
{
$this->classManipulator = $classManipulator;
$this->parsedNodesByType = $parsedNodesByType;
$this->setterOnlyMethodAnalyzer = $setterOnlyMethodAnalyzer;
}
public function getDefinition(): RectorDefinition
@ -88,125 +75,60 @@ CODE_SAMPLE
*/
public function getNodeTypes(): array
{
return [Class_::class];
return [Property::class, MethodCall::class, ClassMethod::class];
}
/**
* @param Class_ $node
* @param Property|MethodCall|ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
$this->methodCallNamesToBeRemoved = [];
$setterOnlyPropertiesAndMethods = $this->resolveSetterOnlyPropertiesAndMethodsForClass($node);
if ($setterOnlyPropertiesAndMethods === null) {
return null;
}
// 1. get assign only private properties
$assignOnlyPrivatePropertyNames = $this->classManipulator->getAssignOnlyPrivatePropertyNames($node);
$this->classManipulator->removeProperties($node, $assignOnlyPrivatePropertyNames);
// 2. remove assigns + class methods with only setter assign
$this->removePropertyAssigns($node, $assignOnlyPrivatePropertyNames);
// 3. remove setter method calls
$this->removeSetterMethodCalls($node);
return $node;
}
/**
* @param string[] $assignOnlyPrivatePropertyNames
*/
private function removePropertyAssigns(Class_ $class, array $assignOnlyPrivatePropertyNames): void
{
$this->traverseNodesWithCallable($class, function (Node $node) use ($assignOnlyPrivatePropertyNames): void {
if ($this->isClassMethodWithSinglePropertyAssignOfNames($node, $assignOnlyPrivatePropertyNames)) {
/** @var string $classMethodName */
$classMethodName = $this->getName($node);
$this->methodCallNamesToBeRemoved[] = $classMethodName;
$this->removeNode($node);
return;
}
if ($this->isPropertyAssignWithPropertyNames($node, $assignOnlyPrivatePropertyNames)) {
// 1. remove class properties
if ($node instanceof Property) {
if ($this->isNames($node, $setterOnlyPropertiesAndMethods['properties'] ?? [])) {
$this->removeNode($node);
}
});
}
}
private function removeSetterMethodCalls(Node $node): void
{
/** @var string $className */
$className = $this->getName($node);
$methodCallsByMethodName = $this->parsedNodesByType->findMethodCallsOnClass($className);
/** @var string $methodName */
foreach ($methodCallsByMethodName as $methodName => $classMethodCalls) {
if (! in_array($methodName, $this->methodCallNamesToBeRemoved, true)) {
continue;
}
foreach ($classMethodCalls as $classMethodCall) {
$this->removeNode($classMethodCall);
// 2. remove class methods
if ($node instanceof ClassMethod) {
if ($this->isNames($node, $setterOnlyPropertiesAndMethods['methods'] ?? [])) {
$this->removeNode($node);
}
}
// 3. remove method calls
if ($node instanceof MethodCall) {
if ($this->isNames($node->name, $setterOnlyPropertiesAndMethods['methods'] ?? [])) {
$this->removeNode($node);
}
}
return null;
}
/**
* Looks for:
*
* public function <someMethod>($value)
* {
* $this->value = $value
* }
*
* @param string[] $propertyNames
* @param Property|ClassMethod|MethodCall $node
* @return string[][]][]|null
*/
private function isClassMethodWithSinglePropertyAssignOfNames(Node $node, array $propertyNames): bool
private function resolveSetterOnlyPropertiesAndMethodsForClass(Node $node): ?array
{
if (! $node instanceof ClassMethod) {
return false;
if ($node instanceof Property || $node instanceof ClassMethod) {
$className = $node->getAttribute(AttributeKey::CLASS_NAME);
} elseif ($node instanceof MethodCall) {
$className = $this->getTypes($node->var)[0] ?? null;
if ($className === null) {
return null;
}
}
if ($this->isName($node, '__construct')) {
return false;
}
$setterOnlyPropertiesAndMethodsByType = $this->setterOnlyMethodAnalyzer->provideSetterOnlyPropertiesAndMethodsByType();
if (count((array) $node->stmts) !== 1) {
return false;
}
if (! $node->stmts[0] instanceof Expression) {
return false;
}
/** @var Expression $onlyExpression */
$onlyExpression = $node->stmts[0];
$onlyStmt = $onlyExpression->expr;
return $this->isPropertyAssignWithPropertyNames($onlyStmt, $propertyNames);
}
/**
* Is: "$this->value = <$value>"
*
* @param string[] $propertyNames
*/
private function isPropertyAssignWithPropertyNames(Node $node, array $propertyNames): bool
{
if (! $node instanceof Assign) {
return false;
}
if (! $node->var instanceof PropertyFetch) {
return false;
}
$propertyFetch = $node->var;
if (! $this->isName($propertyFetch->var, 'this')) {
return false;
}
return $this->isNames($propertyFetch->name, $propertyNames);
return $setterOnlyPropertiesAndMethodsByType[$className] ?? null;
}
}

View File

@ -118,7 +118,7 @@ final class RectorApplication
foreach ($fileInfos as $fileInfo) {
$this->tryCatchWrapper($fileInfo, function (SmartFileInfo $smartFileInfo): void {
$this->fileProcessor->parseFileInfoToLocalCache($smartFileInfo);
});
}, 'parsing');
}
// active only one rule
@ -131,14 +131,14 @@ final class RectorApplication
foreach ($fileInfos as $fileInfo) {
$this->tryCatchWrapper($fileInfo, function (SmartFileInfo $smartFileInfo): void {
$this->fileProcessor->refactor($smartFileInfo);
});
}, 'refactoring');
}
// 3. print to file or string
foreach ($fileInfos as $fileInfo) {
$this->tryCatchWrapper($fileInfo, function (SmartFileInfo $smartFileInfo): void {
$this->processFileInfo($smartFileInfo);
});
}, 'printing');
}
if ($this->configuration->showProgressBar()) {
@ -149,9 +149,9 @@ final class RectorApplication
$this->removedAndAddedFilesProcessor->run();
}
private function tryCatchWrapper(SmartFileInfo $smartFileInfo, callable $callback): void
private function tryCatchWrapper(SmartFileInfo $smartFileInfo, callable $callback, string $phase): void
{
$this->advance($smartFileInfo);
$this->advance($smartFileInfo, $phase);
try {
if (in_array($smartFileInfo, $this->notParsedFiles, true)) {
@ -195,10 +195,10 @@ final class RectorApplication
$this->fileSystemFileProcessor->processFileInfo($fileInfo);
}
private function advance(SmartFileInfo $smartFileInfo): void
private function advance(SmartFileInfo $smartFileInfo, string $phase): void
{
if ($this->symfonyStyle->isVerbose()) {
$this->symfonyStyle->writeln('[parsing] ' . $smartFileInfo->getRealPath());
$this->symfonyStyle->writeln(sprintf('[%s] %s', $phase, $smartFileInfo->getRealPath()));
} elseif ($this->configuration->showProgressBar()) {
$this->symfonyStyle->progressAdvance();
}