Change maximal Rector complexity to 35 to avoid hard coupling to Rector classes (#5206)

* complexity

* decopule GetSubscriberEventsClassMethodFactory and ListenerServiceDefinitionProvider

* decopule UnusedParameterResolver, AssertMethodCallFactory, PropertyFetchWithVariableReplacer

* decopule ParamAndArgFromArrayResolver

* decopule JsonArrayFactory and JsonEncodeStaticCallFactory

* [ci-review] Rector Rectify

Co-authored-by: rector-bot <tomas@getrector.org>
This commit is contained in:
Tomas Votruba 2021-01-16 20:11:11 +01:00 committed by GitHub
parent 83d2e1843f
commit c0105bde25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1058 additions and 644 deletions

View File

@ -56,6 +56,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
$services->alias(SymfonyApplication::class, ConsoleApplication::class);
$services->set(NoRectorsLoadedReporter::class);
$services->set(\Symplify\Astral\NodeTraverser\SimpleCallableNodeTraverser::class);
$services->set(TextDescriptor::class);

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
use Rector\Privatization\Rector\Class_\ChangeLocalPropertyToVariableRector;
use Rector\Privatization\Rector\Class_\ChangeReadOnlyVariableWithDefaultValueToConstantRector;
use Rector\Privatization\Rector\Class_\FinalizeClassesWithoutChildrenRector;
use Rector\Privatization\Rector\Class_\MakeUnusedClassesWithChildrenAbstractRector;
@ -26,6 +27,8 @@ return static function (ContainerConfigurator $containerConfigurator): void {
$services->set(ChangeReadOnlyVariableWithDefaultValueToConstantRector::class);
$services->set(RepeatedLiteralToClassConstantRector::class);
// $services->set(ChangeLocalPropertyToVariableRector::class);
$services->set(PrivatizeLocalOnlyMethodRector::class);
$services->set(PrivatizeLocalGetterToPropertyRector::class);
$services->set(PrivatizeLocalPropertyToPrivatePropertyRector::class);

View File

@ -408,7 +408,7 @@ parameters:
# known values from other methods
-
message: '#Negated boolean expression is always true#'
path: rules/php-spec-to-phpunit/src/Rector/MethodCall/PhpSpecPromisesToPHPUnitAssertRector.php
path: rules/php-spec-to-phpunit/src/NodeFactory/AssertMethodCallFactory.php
-
message: '#Call to function in_array\(\) with arguments string, array\(\) and true will always evaluate to false#'
@ -521,3 +521,5 @@ parameters:
- config/set/*
- '#Class with base "FileNode" name is already used in "PHPStan\\Node\\FileNode", "Rector\\Core\\PhpParser\\Node\\CustomNode\\FileNode"\. Use unique name to make classes easy to recognize#'
- '#Content of method "removeFirstArgument\(\)" is duplicated with method "moveArguments\(\)" in "Rector\\PHPUnit\\Rector\\MethodCall\\AssertSameBoolNullToSpecificMethodRector" class\. Use unique content or abstract service instead#'

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Rector\CodingStyle\NodeAnalyzer;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Scalar\String_;
use Rector\NodeNameResolver\NodeNameResolver;
final class ImplodeAnalyzer
{
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
public function __construct(NodeNameResolver $nodeNameResolver)
{
$this->nodeNameResolver = $nodeNameResolver;
}
/**
* Matches: "implode('","', $items)"
*/
public function isImplodeToJson(Expr $expr): bool
{
if (! $expr instanceof FuncCall) {
return false;
}
if (! $this->nodeNameResolver->isName($expr, 'implode')) {
return false;
}
if (! isset($expr->args[1])) {
return false;
}
$firstArgumentValue = $expr->args[0]->value;
if ($firstArgumentValue instanceof String_ && $firstArgumentValue->value !== '","') {
return false;
}
return true;
}
}

View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Rector\CodingStyle\NodeFactory;
use Nette\Utils\Json;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Scalar\String_;
use Rector\CodingStyle\NodeAnalyzer\ImplodeAnalyzer;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\PhpParser\Node\NodeFactory;
use Symplify\Astral\NodeTraverser\SimpleCallableNodeTraverser;
final class JsonArrayFactory
{
/**
* @var NodeFactory
*/
private $nodeFactory;
/**
* @var ImplodeAnalyzer
*/
private $implodeAnalyzer;
/**
* @var SimpleCallableNodeTraverser
*/
private $simpleCallableNodeTraverser;
public function __construct(
NodeFactory $nodeFactory,
ImplodeAnalyzer $implodeAnalyzer,
SimpleCallableNodeTraverser $simpleCallableNodeTraverser
) {
$this->nodeFactory = $nodeFactory;
$this->implodeAnalyzer = $implodeAnalyzer;
$this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser;
}
public function createFromJsonString(string $stringValue): Array_
{
$array = Json::decode($stringValue, Json::FORCE_ARRAY);
return $this->nodeFactory->createArray($array);
}
/**
* @param Expr[] $placeholderNodes
*/
public function createFromJsonStringAndPlaceholders(string $jsonString, array $placeholderNodes): Array_
{
$jsonArray = $this->createFromJsonString($jsonString);
$this->replaceNodeObjectHashPlaceholdersWithNodes($jsonArray, $placeholderNodes);
return $jsonArray;
}
/**
* @param Expr[] $placeholderNodes
*/
private function replaceNodeObjectHashPlaceholdersWithNodes(Array_ $array, array $placeholderNodes): void
{
// traverse and replace placeholder by original nodes
$this->simpleCallableNodeTraverser->traverseNodesWithCallable($array, function (Node $node) use (
$placeholderNodes
): ?Expr {
if ($node instanceof Array_ && count($node->items) === 1) {
$onlyItem = $node->items[0];
if ($onlyItem === null) {
throw new ShouldNotHappenException();
}
$placeholderNode = $this->matchPlaceholderNode($onlyItem->value, $placeholderNodes);
if ($placeholderNode && $this->implodeAnalyzer->isImplodeToJson($placeholderNode)) {
/** @var FuncCall $placeholderNode */
return $placeholderNode->args[1]->value;
}
}
return $this->matchPlaceholderNode($node, $placeholderNodes);
});
}
/**
* @param Expr[] $placeholderNodes
*/
private function matchPlaceholderNode(Node $node, array $placeholderNodes): ?Expr
{
if (! $node instanceof String_) {
return null;
}
return $placeholderNodes[$node->value] ?? null;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Rector\CodingStyle\NodeFactory;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Variable;
use Rector\Core\PhpParser\Node\NodeFactory;
/**
* Creates + adds
*
* $jsonData = ['...'];
* $json = Nette\Utils\Json::encode($jsonData);
*/
final class JsonEncodeStaticCallFactory
{
/**
* @var NodeFactory
*/
private $nodeFactory;
public function __construct(NodeFactory $nodeFactory)
{
$this->nodeFactory = $nodeFactory;
}
/**
* Creates + adds
*
* $jsonData = ['...'];
* $json = Nette\Utils\Json::encode($jsonData);
*/
public function createFromArray(Expr $assignExpr, Array_ $jsonArray): Assign
{
$jsonDataAssign = new Assign($assignExpr, $jsonArray);
$jsonDataVariable = new Variable('jsonData');
$jsonDataAssign->expr = $this->nodeFactory->createStaticCall('Nette\Utils\Json', 'encode', [$jsonDataVariable]);
return $jsonDataAssign;
}
}

View File

@ -9,19 +9,18 @@ use Nette\Utils\JsonException;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\AssignOp\Concat as ConcatAssign;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use Rector\CodingStyle\Node\ConcatJoiner;
use Rector\CodingStyle\Node\ConcatManipulator;
use Rector\CodingStyle\NodeFactory\JsonArrayFactory;
use Rector\CodingStyle\NodeFactory\JsonEncodeStaticCallFactory;
use Rector\CodingStyle\ValueObject\ConcatExpressionJoinData;
use Rector\CodingStyle\ValueObject\NodeToRemoveAndConcatItem;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Rector\AbstractRector;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
@ -56,10 +55,26 @@ final class ManualJsonStringToJsonEncodeArrayRector extends AbstractRector
*/
private $concatManipulator;
public function __construct(ConcatJoiner $concatJoiner, ConcatManipulator $concatManipulator)
{
/**
* @var JsonEncodeStaticCallFactory
*/
private $jsonEncodeStaticCallFactory;
/**
* @var JsonArrayFactory
*/
private $jsonArrayFactory;
public function __construct(
ConcatJoiner $concatJoiner,
ConcatManipulator $concatManipulator,
JsonEncodeStaticCallFactory $jsonEncodeStaticCallFactory,
JsonArrayFactory $jsonArrayFactory
) {
$this->concatJoiner = $concatJoiner;
$this->concatManipulator = $concatManipulator;
$this->jsonEncodeStaticCallFactory = $jsonEncodeStaticCallFactory;
$this->jsonArrayFactory = $jsonArrayFactory;
}
public function getRuleDefinition(): RuleDefinition
@ -77,6 +92,8 @@ final class SomeClass
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use Nette\Utils\Json;
final class SomeClass
{
public function run()
@ -85,8 +102,7 @@ final class SomeClass
'role_name' => 'admin',
'numberz' => ['id' => 10]
];
$someJsonAsString = Nette\Utils\Json::encode($data);
$someJsonAsString = Json::encode($data);
}
}
CODE_SAMPLE
@ -113,7 +129,15 @@ CODE_SAMPLE
// A. full json string
$isJsonString = $this->isJsonString($stringValue);
if ($isJsonString) {
return $this->processJsonString($node, $stringValue);
$jsonArray = $this->jsonArrayFactory->createFromJsonString($stringValue);
$jsonEncodeAssign = $this->jsonEncodeStaticCallFactory->createFromArray($node->var, $jsonArray);
$jsonDataVariable = new Variable('jsonData');
$jsonDataAssign = new Assign($jsonDataVariable, $jsonArray);
$this->addNodeBeforeNode($jsonDataAssign, $node);
return $jsonEncodeAssign;
}
// B. just start of a json? join with all the strings that concat so same variable
@ -173,13 +197,6 @@ CODE_SAMPLE
}
}
private function processJsonString(Assign $assign, string $stringValue): Node
{
$arrayNode = $this->createArrayNodeFromJsonString($stringValue);
return $this->createAndReturnJsonEncodeFromArray($assign, $arrayNode);
}
private function collectContentAndPlaceholderNodesFromNextExpressions(Assign $assign): ConcatExpressionJoinData
{
$concatExpressionJoinData = new ConcatExpressionJoinData();
@ -239,35 +256,13 @@ CODE_SAMPLE
$this->removeNodes($nodesToRemove);
$jsonArray = $this->createArrayNodeFromJsonString($stringValue);
$this->replaceNodeObjectHashPlaceholdersWithNodes($jsonArray, $placeholderNodes);
return $this->createAndReturnJsonEncodeFromArray($assign, $jsonArray);
}
private function createArrayNodeFromJsonString(string $stringValue): Array_
{
$array = Json::decode($stringValue, Json::FORCE_ARRAY);
return $this->createArray($array);
}
/**
* Creates + adds
*
* $jsonData = ['...'];
* $json = Nette\Utils\Json::encode($jsonData);
*/
private function createAndReturnJsonEncodeFromArray(Assign $assign, Array_ $jsonArray): Assign
{
$jsonArray = $this->jsonArrayFactory->createFromJsonStringAndPlaceholders($stringValue, $placeholderNodes);
$jsonDataVariable = new Variable('jsonData');
$jsonDataAssign = new Assign($jsonDataVariable, $jsonArray);
$this->addNodeBeforeNode($jsonDataAssign, $assign);
$assign->expr = $this->createStaticCall('Nette\Utils\Json', 'encode', [$jsonDataVariable]);
return $assign;
return $this->jsonEncodeStaticCallFactory->createFromArray($assign->var, $jsonArray);
}
/**
@ -316,66 +311,4 @@ CODE_SAMPLE
return null;
}
/**
* @param Expr[] $placeholderNodes
*/
private function replaceNodeObjectHashPlaceholdersWithNodes(Array_ $array, array $placeholderNodes): void
{
// traverse and replace placeholder by original nodes
$this->traverseNodesWithCallable($array, function (Node $node) use ($placeholderNodes): ?Expr {
if ($node instanceof Array_ && count($node->items) === 1) {
$onlyItem = $node->items[0];
if ($onlyItem === null) {
throw new ShouldNotHappenException();
}
$placeholderNode = $this->matchPlaceholderNode($onlyItem->value, $placeholderNodes);
if ($placeholderNode && $this->isImplodeToJson($placeholderNode)) {
/** @var FuncCall $placeholderNode */
return $placeholderNode->args[1]->value;
}
}
return $this->matchPlaceholderNode($node, $placeholderNodes);
});
}
/**
* @param Expr[] $placeholderNodes
*/
private function matchPlaceholderNode(Node $node, array $placeholderNodes): ?Expr
{
if (! $node instanceof String_) {
return null;
}
return $placeholderNodes[$node->value] ?? null;
}
/**
* Matches: "implode('","', $items)"
*/
private function isImplodeToJson(Expr $expr): bool
{
if (! $expr instanceof FuncCall) {
return false;
}
if (! $this->isName($expr, 'implode')) {
return false;
}
if (! isset($expr->args[1])) {
return false;
}
$firstArgumentValue = $expr->args[0]->value;
if ($firstArgumentValue instanceof String_ && $firstArgumentValue->value !== '","') {
return false;
}
return true;
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Rector\DeadCode\NodeCollector;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Core\PhpParser\Node\Manipulator\ClassMethodManipulator;
use Rector\Core\PhpParser\Printer\BetterStandardPrinter;
use Rector\Core\ValueObject\MethodName;
use Rector\NodeNameResolver\NodeNameResolver;
final class UnusedParameterResolver
{
/**
* @var ClassMethodManipulator
*/
private $classMethodManipulator;
/**
* @var BetterStandardPrinter
*/
private $betterStandardPrinter;
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
public function __construct(
ClassMethodManipulator $classMethodManipulator,
NodeNameResolver $nodeNameResolver,
BetterStandardPrinter $betterStandardPrinter
) {
$this->classMethodManipulator = $classMethodManipulator;
$this->betterStandardPrinter = $betterStandardPrinter;
$this->nodeNameResolver = $nodeNameResolver;
}
/**
* @param Class_[] $childrenOfClass
* @return Param[]
*/
public function resolve(ClassMethod $classMethod, string $methodName, array $childrenOfClass): array
{
$unusedParameters = $this->resolveUnusedParameters($classMethod);
if ($unusedParameters === []) {
return [];
}
foreach ($childrenOfClass as $childClassNode) {
$methodOfChild = $childClassNode->getMethod($methodName);
if ($methodOfChild === null) {
continue;
}
$unusedParameters = $this->getParameterOverlap(
$unusedParameters,
$this->resolveUnusedParameters($methodOfChild)
);
}
return $unusedParameters;
}
/**
* @return Param[]
*/
private function resolveUnusedParameters(ClassMethod $classMethod): array
{
$unusedParameters = [];
foreach ($classMethod->params as $i => $param) {
// skip property promotion
/** @var Param $param */
if ($param->flags !== 0) {
continue;
}
if ($this->classMethodManipulator->isParameterUsedInClassMethod($param, $classMethod)) {
// reset to keep order of removed arguments, if not construtctor - probably autowired
if (! $this->nodeNameResolver->isName($classMethod, MethodName::CONSTRUCT)) {
$unusedParameters = [];
}
continue;
}
$unusedParameters[$i] = $param;
}
return $unusedParameters;
}
/**
* @param Param[] $parameters1
* @param Param[] $parameters2
* @return Param[]
*/
private function getParameterOverlap(array $parameters1, array $parameters2): array
{
return array_uintersect(
$parameters1,
$parameters2,
function (Param $firstParam, Param $secondParam): int {
return $this->betterStandardPrinter->areNodesEqual($firstParam, $secondParam) ? 0 : 1;
}
);
}
}

View File

@ -11,9 +11,8 @@ use PhpParser\Node\Stmt\ClassMethod;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\Core\PhpParser\Node\Manipulator\ClassMethodManipulator;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\MethodName;
use Rector\DeadCode\NodeCollector\UnusedParameterResolver;
use Rector\DeadCode\NodeManipulator\MagicMethodDetector;
use Rector\DeadCode\NodeManipulator\VariadicFunctionLikeDetector;
use Rector\NodeTypeResolver\Node\AttributeKey;
@ -33,11 +32,6 @@ final class RemoveUnusedParameterRector extends AbstractRector implements ZeroCa
*/
private $classManipulator;
/**
* @var ClassMethodManipulator
*/
private $classMethodManipulator;
/**
* @var MagicMethodDetector
*/
@ -48,16 +42,21 @@ final class RemoveUnusedParameterRector extends AbstractRector implements ZeroCa
*/
private $variadicFunctionLikeDetector;
/**
* @var UnusedParameterResolver
*/
private $unusedParameterResolver;
public function __construct(
ClassManipulator $classManipulator,
ClassMethodManipulator $classMethodManipulator,
MagicMethodDetector $magicMethodDetector,
VariadicFunctionLikeDetector $variadicFunctionLikeDetector
VariadicFunctionLikeDetector $variadicFunctionLikeDetector,
UnusedParameterResolver $unusedParameterResolver
) {
$this->classManipulator = $classManipulator;
$this->classMethodManipulator = $classMethodManipulator;
$this->magicMethodDetector = $magicMethodDetector;
$this->variadicFunctionLikeDetector = $variadicFunctionLikeDetector;
$this->unusedParameterResolver = $unusedParameterResolver;
}
public function getRuleDefinition(): RuleDefinition
@ -119,7 +118,7 @@ CODE_SAMPLE
}
$childrenOfClass = $this->nodeRepository->findChildrenOfClass($className);
$unusedParameters = $this->getUnusedParameters($node, $methodName, $childrenOfClass);
$unusedParameters = $this->unusedParameterResolver->resolve($node, $methodName, $childrenOfClass);
if ($unusedParameters === []) {
return null;
}
@ -176,48 +175,6 @@ CODE_SAMPLE
return $this->isAnonymousClass($classLike);
}
/**
* @param Class_[] $childrenOfClass
* @return Param[]
*/
private function getUnusedParameters(ClassMethod $classMethod, string $methodName, array $childrenOfClass): array
{
$unusedParameters = $this->resolveUnusedParameters($classMethod);
if ($unusedParameters === []) {
return [];
}
foreach ($childrenOfClass as $childClassNode) {
$methodOfChild = $childClassNode->getMethod($methodName);
if ($methodOfChild === null) {
continue;
}
$unusedParameters = $this->getParameterOverlap(
$unusedParameters,
$this->resolveUnusedParameters($methodOfChild)
);
}
return $unusedParameters;
}
/**
* @param Param[] $parameters1
* @param Param[] $parameters2
* @return Param[]
*/
private function getParameterOverlap(array $parameters1, array $parameters2): array
{
return array_uintersect(
$parameters1,
$parameters2,
function (Param $firstParam, Param $secondParam): int {
return $this->areNodesEqual($firstParam, $secondParam) ? 0 : 1;
}
);
}
/**
* @param Param[] $unusedParameters
*/
@ -301,31 +258,18 @@ CODE_SAMPLE
}
/**
* @param Param[] $parameters1
* @param Param[] $parameters2
* @return Param[]
*/
private function resolveUnusedParameters(ClassMethod $classMethod): array
private function getParameterOverlap(array $parameters1, array $parameters2): array
{
$unusedParameters = [];
foreach ($classMethod->params as $i => $param) {
// skip property promotion
/** @var Param $param */
if ($param->flags !== 0) {
continue;
return array_uintersect(
$parameters1,
$parameters2,
function (Param $firstParam, Param $secondParam): int {
return $this->betterStandardPrinter->areNodesEqual($firstParam, $secondParam) ? 0 : 1;
}
if ($this->classMethodManipulator->isParameterUsedInClassMethod($param, $classMethod)) {
// reset to keep order of removed arguments, if not construtctor - probably autowired
if (! $this->isName($classMethod, MethodName::CONSTRUCT)) {
$unusedParameters = [];
}
continue;
}
$unusedParameters[$i] = $param;
}
return $unusedParameters;
);
}
}

View File

@ -2,7 +2,7 @@
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class StaticCall
class SomeStaticCall
{
public static function run()
{
@ -16,7 +16,7 @@ class StaticCall
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\Fixture;
class StaticCall
class SomeStaticCall
{
}

View File

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Rector\PhpSpecToPHPUnit\NodeFactory;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use Rector\Core\PhpParser\Node\Manipulator\ConstFetchManipulator;
use Rector\Core\PhpParser\Node\NodeFactory;
use Rector\NodeNameResolver\NodeNameResolver;
final class AssertMethodCallFactory
{
/**
* @var NodeFactory
*/
private $nodeFactory;
/**
* @var ConstFetchManipulator
*/
private $constFetchManipulator;
/**
* @var bool
*/
private $isBoolAssert = false;
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
public function __construct(
NodeFactory $nodeFactory,
ConstFetchManipulator $constFetchManipulator,
NodeNameResolver $nodeNameResolver
) {
$this->nodeFactory = $nodeFactory;
$this->constFetchManipulator = $constFetchManipulator;
$this->nodeNameResolver = $nodeNameResolver;
}
public function createAssertMethod(
string $name,
Expr $value,
?Expr $expected,
PropertyFetch $testedObjectPropertyFetch
): MethodCall {
$this->isBoolAssert = false;
// special case with bool!
if ($expected !== null) {
$name = $this->resolveBoolMethodName($name, $expected);
}
$assetMethodCall = $this->nodeFactory->createMethodCall('this', $name);
if (! $this->isBoolAssert && $expected) {
$assetMethodCall->args[] = new Arg($this->thisToTestedObjectPropertyFetch(
$expected,
$testedObjectPropertyFetch
));
}
$assetMethodCall->args[] = new Arg($this->thisToTestedObjectPropertyFetch($value, $testedObjectPropertyFetch));
return $assetMethodCall;
}
private function resolveBoolMethodName(string $name, Expr $expr): string
{
if (! $this->constFetchManipulator->isBool($expr)) {
return $name;
}
if ($name === 'assertSame') {
$this->isBoolAssert = true;
return $this->constFetchManipulator->isFalse($expr) ? 'assertFalse' : 'assertTrue';
}
if ($name === 'assertNotSame') {
$this->isBoolAssert = true;
return $this->constFetchManipulator->isFalse($expr) ? 'assertNotFalse' : 'assertNotTrue';
}
return $name;
}
private function thisToTestedObjectPropertyFetch(Expr $expr, PropertyFetch $propertyFetch): Expr
{
if (! $expr instanceof Variable) {
return $expr;
}
if (! $this->nodeNameResolver->isName($expr, 'this')) {
return $expr;
}
return $propertyFetch;
}
}

View File

@ -6,7 +6,6 @@ namespace Rector\PhpSpecToPHPUnit\Rector\MethodCall;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\ArrayItem;
@ -26,6 +25,7 @@ use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpSpecToPHPUnit\MatchersManipulator;
use Rector\PhpSpecToPHPUnit\Naming\PhpSpecRenaming;
use Rector\PhpSpecToPHPUnit\NodeFactory\AssertMethodCallFactory;
use Rector\PhpSpecToPHPUnit\Rector\AbstractPhpSpecToPHPUnitRector;
/**
@ -37,7 +37,7 @@ final class PhpSpecPromisesToPHPUnitAssertRector extends AbstractPhpSpecToPHPUni
* @see https://github.com/phpspec/phpspec/blob/master/src/PhpSpec/Wrapper/Subject.php
*
* @see https://phpunit.readthedocs.io/en/8.0/assertions.html
* @var string[][]
* @var array<string, string[]>
*/
private const NEW_METHOD_TO_OLD_METHODS = [
'assertInstanceOf' => ['shouldBeAnInstanceOf', 'shouldHaveType', 'shouldReturnAnInstanceOf'],
@ -86,11 +86,6 @@ final class PhpSpecPromisesToPHPUnitAssertRector extends AbstractPhpSpecToPHPUni
*/
private $testedClass;
/**
* @var bool
*/
private $isBoolAssert = false;
/**
* @var bool
*/
@ -116,10 +111,19 @@ final class PhpSpecPromisesToPHPUnitAssertRector extends AbstractPhpSpecToPHPUni
*/
private $matchersManipulator;
public function __construct(MatchersManipulator $matchersManipulator, PhpSpecRenaming $phpSpecRenaming)
{
/**
* @var AssertMethodCallFactory
*/
private $assertMethodCallFactory;
public function __construct(
MatchersManipulator $matchersManipulator,
PhpSpecRenaming $phpSpecRenaming,
AssertMethodCallFactory $assertMethodCallFactory
) {
$this->phpSpecRenaming = $phpSpecRenaming;
$this->matchersManipulator = $matchersManipulator;
$this->assertMethodCallFactory = $assertMethodCallFactory;
}
/**
@ -147,7 +151,7 @@ final class PhpSpecPromisesToPHPUnitAssertRector extends AbstractPhpSpecToPHPUni
}
if ($this->isName($node->name, 'during')) {
return $this->processDuring($node);
return $this->processDuringMethodCall($node);
}
if ($this->isName($node->name, 'duringInstantiation')) {
@ -168,7 +172,12 @@ final class PhpSpecPromisesToPHPUnitAssertRector extends AbstractPhpSpecToPHPUni
foreach (self::NEW_METHOD_TO_OLD_METHODS as $newMethod => $oldMethods) {
if ($this->isNames($node->name, $oldMethods)) {
return $this->createAssertMethod($newMethod, $node->var, $node->args[0]->value ?? null);
return $this->assertMethodCallFactory->createAssertMethod(
$newMethod,
$node->var,
$node->args[0]->value ?? null,
$this->testedObjectPropertyFetch
);
}
}
@ -197,7 +206,7 @@ final class PhpSpecPromisesToPHPUnitAssertRector extends AbstractPhpSpecToPHPUni
return $node;
}
private function processDuring(MethodCall $methodCall): MethodCall
private function processDuringMethodCall(MethodCall $methodCall): MethodCall
{
if (! isset($methodCall->args[0])) {
throw new ShouldNotHappenException();
@ -308,26 +317,6 @@ final class PhpSpecPromisesToPHPUnitAssertRector extends AbstractPhpSpecToPHPUni
}
}
private function createAssertMethod(string $name, Expr $value, ?Expr $expected): MethodCall
{
$this->isBoolAssert = false;
// special case with bool!
if ($expected !== null) {
$name = $this->resolveBoolMethodName($name, $expected);
}
$assetMethodCall = $this->createMethodCall(self::THIS, $name);
if (! $this->isBoolAssert && $expected) {
$assetMethodCall->args[] = new Arg($this->thisToTestedObjectPropertyFetch($expected));
}
$assetMethodCall->args[] = new Arg($this->thisToTestedObjectPropertyFetch($value));
return $assetMethodCall;
}
private function shouldSkip(MethodCall $methodCall): bool
{
if (! $this->isVariableName($methodCall->var, self::THIS)) {
@ -365,32 +354,4 @@ final class PhpSpecPromisesToPHPUnitAssertRector extends AbstractPhpSpecToPHPUni
$staticCall->args[] = new Arg($arrayItem->value);
}
}
private function resolveBoolMethodName(string $name, Expr $expr): string
{
if (! $this->isBool($expr)) {
return $name;
}
if ($name === 'assertSame') {
$this->isBoolAssert = true;
return $this->isFalse($expr) ? 'assertFalse' : 'assertTrue';
}
if ($name === 'assertNotSame') {
$this->isBoolAssert = true;
return $this->isFalse($expr) ? 'assertNotFalse' : 'assertNotTrue';
}
return $name;
}
private function thisToTestedObjectPropertyFetch(Expr $expr): Expr
{
if (! $this->isVariableName($expr, self::THIS)) {
return $expr;
}
return $this->testedObjectPropertyFetch;
}
}

View File

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Rector\PHPUnit\NodeManipulator;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\Variable;
use PHPStan\Type\Type;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Rector\PHPUnit\ValueObject\ParamAndArg;
final class ParamAndArgFromArrayResolver
{
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
/**
* @var TypeFactory
*/
private $typeFactory;
public function __construct(NodeTypeResolver $nodeTypeResolver, TypeFactory $typeFactory)
{
$this->nodeTypeResolver = $nodeTypeResolver;
$this->typeFactory = $typeFactory;
}
/**
* @return ParamAndArg[]
*/
public function resolve(Array_ $array, string $variableName): array
{
$isNestedArray = $this->isNestedArray($array);
if ($isNestedArray) {
return $this->collectParamAndArgsFromNestedArray($array, $variableName);
}
$itemsStaticType = $this->resolveItemStaticType($array, $isNestedArray);
return $this->collectParamAndArgsFromNonNestedArray($array, $variableName, $itemsStaticType);
}
private function isNestedArray(Array_ $array): bool
{
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
if ($arrayItem->value instanceof Array_) {
return true;
}
}
return false;
}
/**
* @return ParamAndArg[]
*/
private function collectParamAndArgsFromNestedArray(Array_ $array, string $variableName): array
{
$paramAndArgs = [];
$i = 1;
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
$nestedArray = $arrayItem->value;
if (! $nestedArray instanceof Array_) {
continue;
}
foreach ($nestedArray->items as $nestedArrayItem) {
if (! $nestedArrayItem instanceof ArrayItem) {
continue;
}
$variable = new Variable($variableName . ($i === 1 ? '' : $i));
$itemsStaticType = $this->nodeTypeResolver->getStaticType($nestedArrayItem->value);
$paramAndArgs[] = new ParamAndArg($variable, $itemsStaticType);
++$i;
}
}
return $paramAndArgs;
}
private function resolveItemStaticType(Array_ $array, bool $isNestedArray): Type
{
$staticTypes = [];
if (! $isNestedArray) {
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
$staticTypes[] = $this->nodeTypeResolver->getStaticType($arrayItem->value);
}
}
return $this->typeFactory->createMixedPassedOrUnionType($staticTypes);
}
/**
* @return ParamAndArg[]
*/
private function collectParamAndArgsFromNonNestedArray(
Array_ $array,
string $variableName,
Type $itemsStaticType
): array {
$i = 1;
$paramAndArgs = [];
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
$variable = new Variable($variableName . ($i === 1 ? '' : $i));
$paramAndArgs[] = new ParamAndArg($variable, $itemsStaticType);
++$i;
if (! $arrayItem->value instanceof Array_) {
break;
}
}
return $paramAndArgs;
}
}

View File

@ -7,9 +7,7 @@ namespace Rector\PHPUnit\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Class_;
@ -17,7 +15,6 @@ use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\AttributeAwarePhpDoc\Ast\PhpDoc\AttributeAwareParamTagValueNode;
use Rector\AttributeAwarePhpDoc\Ast\PhpDoc\AttributeAwarePhpDocTagNode;
@ -26,8 +23,8 @@ use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Rector\AbstractPHPUnitRector;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
use Rector\PHPUnit\NodeFactory\DataProviderClassMethodFactory;
use Rector\PHPUnit\NodeManipulator\ParamAndArgFromArrayResolver;
use Rector\PHPUnit\ValueObject\ArrayArgumentToDataProvider;
use Rector\PHPUnit\ValueObject\DataProviderClassMethodRecipe;
use Rector\PHPUnit\ValueObject\ParamAndArg;
@ -64,16 +61,16 @@ final class ArrayArgumentInTestToDataProviderRector extends AbstractPHPUnitRecto
private $dataProviderClassMethodFactory;
/**
* @var TypeFactory
* @var ParamAndArgFromArrayResolver
*/
private $typeFactory;
private $paramAndArgFromArrayResolver;
public function __construct(
DataProviderClassMethodFactory $dataProviderClassMethodFactory,
TypeFactory $typeFactory
ParamAndArgFromArrayResolver $paramAndArgFromArrayResolver
) {
$this->dataProviderClassMethodFactory = $dataProviderClassMethodFactory;
$this->typeFactory = $typeFactory;
$this->paramAndArgFromArrayResolver = $paramAndArgFromArrayResolver;
}
public function getRuleDefinition(): RuleDefinition
@ -209,7 +206,7 @@ CODE_SAMPLE
$methodCall->args = [];
$paramAndArgs = $this->collectParamAndArgsFromArray(
$paramAndArgs = $this->paramAndArgFromArrayResolver->resolve(
$firstArgumentValue,
$arrayArgumentToDataProvider->getVariableName()
);
@ -266,20 +263,6 @@ CODE_SAMPLE
return 'provideDataFor' . ucfirst($methodName);
}
/**
* @return ParamAndArg[]
*/
private function collectParamAndArgsFromArray(Array_ $array, string $variableName): array
{
$isNestedArray = $this->isNestedArray($array);
if ($isNestedArray) {
return $this->collectParamAndArgsFromNestedArray($array, $variableName);
}
$itemsStaticType = $this->resolveItemStaticType($array, $isNestedArray);
return $this->collectParamAndArgsFromNonNestedArray($array, $variableName, $itemsStaticType);
}
/**
* @param ParamAndArg[] $paramAndArgs
*/
@ -315,99 +298,6 @@ CODE_SAMPLE
));
}
private function isNestedArray(Array_ $array): bool
{
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
if ($arrayItem->value instanceof Array_) {
return true;
}
}
return false;
}
/**
* @return ParamAndArg[]
*/
private function collectParamAndArgsFromNestedArray(Array_ $array, string $variableName): array
{
$paramAndArgs = [];
$i = 1;
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
$nestedArray = $arrayItem->value;
if (! $nestedArray instanceof Array_) {
continue;
}
foreach ($nestedArray->items as $nestedArrayItem) {
if (! $nestedArrayItem instanceof ArrayItem) {
continue;
}
$variable = new Variable($variableName . ($i === 1 ? '' : $i));
$itemsStaticType = $this->getStaticType($nestedArrayItem->value);
$paramAndArgs[] = new ParamAndArg($variable, $itemsStaticType);
++$i;
}
}
return $paramAndArgs;
}
private function resolveItemStaticType(Array_ $array, bool $isNestedArray): Type
{
$staticTypes = [];
if (! $isNestedArray) {
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
$staticTypes[] = $this->getStaticType($arrayItem->value);
}
}
return $this->typeFactory->createMixedPassedOrUnionType($staticTypes);
}
/**
* @return ParamAndArg[]
*/
private function collectParamAndArgsFromNonNestedArray(
Array_ $array,
string $variableName,
Type $itemsStaticType
): array {
$i = 1;
$paramAndArgs = [];
foreach ($array->items as $arrayItem) {
if (! $arrayItem instanceof ArrayItem) {
continue;
}
$variable = new Variable($variableName . ($i === 1 ? '' : $i));
$paramAndArgs[] = new ParamAndArg($variable, $itemsStaticType);
++$i;
if (! $arrayItem->value instanceof Array_) {
break;
}
}
return $paramAndArgs;
}
/**
* @param ParamAndArg[] $paramAndArgs
* @return Param[]
@ -417,7 +307,6 @@ CODE_SAMPLE
$params = [];
foreach ($paramAndArgs as $paramAndArg) {
$param = new Param($paramAndArg->getVariable());
$this->setTypeIfNotNull($paramAndArg, $param);
$params[] = $param;

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Rector\Privatization\NodeReplacer;
use PhpParser\Node;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Class_;
use Rector\NodeNameResolver\NodeNameResolver;
use Symplify\Astral\NodeTraverser\SimpleCallableNodeTraverser;
final class PropertyFetchWithVariableReplacer
{
/**
* @var SimpleCallableNodeTraverser
*/
private $simpleCallableNodeTraverser;
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
public function __construct(
SimpleCallableNodeTraverser $simpleCallableNodeTraverser,
NodeNameResolver $nodeNameResolver
) {
$this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser;
$this->nodeNameResolver = $nodeNameResolver;
}
/**
* @param array<string, string[]> $methodsByPropertyName
*/
public function replacePropertyFetchesByVariable(Class_ $class, array $methodsByPropertyName): void
{
foreach ($methodsByPropertyName as $propertyName => $methodNames) {
$methodName = $methodNames[0];
$classMethod = $class->getMethod($methodName);
if ($classMethod === null) {
continue;
}
$this->simpleCallableNodeTraverser->traverseNodesWithCallable(
(array) $classMethod->getStmts(),
function (Node $node) use ($propertyName): ?Variable {
if (! $node instanceof PropertyFetch) {
return null;
}
if (! $this->nodeNameResolver->isName($node, $propertyName)) {
return null;
}
return new Variable($propertyName);
}
);
}
}
}

View File

@ -7,7 +7,6 @@ namespace Rector\Privatization\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Do_;
@ -18,7 +17,9 @@ use PhpParser\NodeTraverser;
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\Core\PhpParser\Node\Manipulator\PropertyFetchManipulator;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\Util\StaticInstanceOf;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Privatization\NodeReplacer\PropertyFetchWithVariableReplacer;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@ -42,10 +43,19 @@ final class ChangeLocalPropertyToVariableRector extends AbstractRector
*/
private $propertyFetchManipulator;
public function __construct(ClassManipulator $classManipulator, PropertyFetchManipulator $propertyFetchManipulator)
{
/**
* @var PropertyFetchWithVariableReplacer
*/
private $propertyFetchWithVariableReplacer;
public function __construct(
ClassManipulator $classManipulator,
PropertyFetchManipulator $propertyFetchManipulator,
PropertyFetchWithVariableReplacer $propertyFetchWithVariableReplacer
) {
$this->classManipulator = $classManipulator;
$this->propertyFetchManipulator = $propertyFetchManipulator;
$this->propertyFetchWithVariableReplacer = $propertyFetchWithVariableReplacer;
}
public function getRuleDefinition(): RuleDefinition
@ -109,7 +119,7 @@ CODE_SAMPLE
unset($propertyUsageByMethods[$propertyName]);
}
$this->replacePropertyFetchesByLocalProperty($node, $propertyUsageByMethods);
$this->propertyFetchWithVariableReplacer->replacePropertyFetchesByVariable($node, $propertyUsageByMethods);
// remove properties
foreach ($node->getProperties() as $property) {
@ -163,34 +173,6 @@ CODE_SAMPLE
return $propertyUsageByMethods;
}
/**
* @param string[][] $propertyUsageByMethods
*/
private function replacePropertyFetchesByLocalProperty(Class_ $class, array $propertyUsageByMethods): void
{
foreach ($propertyUsageByMethods as $propertyName => $methodNames) {
$methodName = $methodNames[0];
$classMethod = $class->getMethod($methodName);
if ($classMethod === null) {
continue;
}
$this->traverseNodesWithCallable((array) $classMethod->getStmts(), function (Node $node) use (
$propertyName
): ?Variable {
if (! $node instanceof PropertyFetch) {
return null;
}
if (! $this->isName($node, $propertyName)) {
return null;
}
return new Variable($propertyName);
});
}
}
/**
* Covers https://github.com/rectorphp/rector/pull/2558#discussion_r363036110
*/
@ -240,15 +222,7 @@ CODE_SAMPLE
private function isScopeChangingNode(Node $node): bool
{
foreach (self::SCOPE_CHANGING_NODE_TYPES as $scopeChangingNode) {
if (! is_a($node, $scopeChangingNode, true)) {
continue;
}
return true;
}
return false;
return StaticInstanceOf::isOneOf($node, self::SCOPE_CHANGING_NODE_TYPES);
}
private function refactorIf(If_ $if, string $privatePropertyName): ?bool

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Rector\SymfonyCodeQuality\ApplicationMetadata;
use Nette\Utils\Strings;
use Rector\Symfony\ServiceMapProvider;
use Rector\Symfony\ValueObject\ServiceDefinition;
use Rector\Symfony\ValueObject\Tag\EventListenerTag;
final class ListenerServiceDefinitionProvider
{
/**
* @var string
* @see https://regex101.com/r/j6SAga/1
*/
private const SYMFONY_FAMILY_REGEX = '#^(Symfony|Sensio|Doctrine)\b#';
/**
* @var bool
*/
private $areListenerClassesLoaded = false;
/**
* @var ServiceDefinition[][][]
*/
private $listenerClassesToEvents = [];
/**
* @var ServiceMapProvider
*/
private $applicationServiceMapProvider;
public function __construct(ServiceMapProvider $applicationServiceMapProvider)
{
$this->applicationServiceMapProvider = $applicationServiceMapProvider;
}
/**
* @return ServiceDefinition[][][]
*/
public function extract(): array
{
if ($this->areListenerClassesLoaded) {
return $this->listenerClassesToEvents;
}
$serviceMap = $this->applicationServiceMapProvider->provide();
$eventListeners = $serviceMap->getServicesByTag('kernel.event_listener');
foreach ($eventListeners as $eventListener) {
// skip Symfony core listeners
if (Strings::match((string) $eventListener->getClass(), self::SYMFONY_FAMILY_REGEX)) {
continue;
}
foreach ($eventListener->getTags() as $tag) {
if (! $tag instanceof EventListenerTag) {
continue;
}
$eventName = $tag->getEvent();
$this->listenerClassesToEvents[$eventListener->getClass()][$eventName][] = $eventListener;
}
}
$this->areListenerClassesLoaded = true;
return $this->listenerClassesToEvents;
}
}

View File

@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
namespace Rector\SymfonyCodeQuality\NodeFactory;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Type\ArrayType;
use PHPStan\Type\MixedType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\Core\Php\PhpVersionProvider;
use Rector\Core\PhpParser\Node\Manipulator\VisibilityManipulator;
use Rector\Core\PhpParser\Node\NodeFactory;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Symfony\Contract\Tag\TagInterface;
use Rector\Symfony\ValueObject\ServiceDefinition;
use Rector\Symfony\ValueObject\Tag;
use Rector\Symfony\ValueObject\Tag\EventListenerTag;
use Rector\SymfonyCodeQuality\ValueObject\EventNameToClassAndConstant;
final class GetSubscriberEventsClassMethodFactory
{
/**
* @var NodeFactory
*/
private $nodeFactory;
/**
* @var VisibilityManipulator
*/
private $visibilityManipulator;
/**
* @var PhpVersionProvider
*/
private $phpVersionProvider;
/**
* @var PhpDocInfoFactory
*/
private $phpDocInfoFactory;
public function __construct(
NodeFactory $nodeFactory,
VisibilityManipulator $visibilityManipulator,
PhpVersionProvider $phpVersionProvider,
PhpDocInfoFactory $phpDocInfoFactory
) {
$this->nodeFactory = $nodeFactory;
$this->visibilityManipulator = $visibilityManipulator;
$this->phpVersionProvider = $phpVersionProvider;
$this->phpDocInfoFactory = $phpDocInfoFactory;
}
/**
* @param array<string, ServiceDefinition[]> $eventsToMethods
* @param EventNameToClassAndConstant[] $eventNamesToClassConstants
*/
public function createFromEventsToMethods(array $eventsToMethods, array $eventNamesToClassConstants): ClassMethod
{
$getSubscribersClassMethod = $this->nodeFactory->createPublicMethod('getSubscribedEvents');
$eventsToMethodsArray = new Array_();
$this->visibilityManipulator->makeStatic($getSubscribersClassMethod);
foreach ($eventsToMethods as $eventName => $methodNamesWithPriorities) {
$eventNameExpr = $this->createEventName($eventName, $eventNamesToClassConstants);
if (count($methodNamesWithPriorities) === 1) {
$this->createSingleMethod(
$methodNamesWithPriorities,
$eventName,
$eventNameExpr,
$eventsToMethodsArray
);
} else {
$this->createMultipleMethods(
$methodNamesWithPriorities,
$eventNameExpr,
$eventsToMethodsArray,
$eventName
);
}
}
$getSubscribersClassMethod->stmts[] = new Return_($eventsToMethodsArray);
$this->decorateClassMethodWithReturnType($getSubscribersClassMethod);
return $getSubscribersClassMethod;
}
/**
* @param ClassConstFetch|String_ $expr
* @param ServiceDefinition[] $methodNamesWithPriorities
*/
private function createSingleMethod(
array $methodNamesWithPriorities,
string $eventName,
Expr $expr,
Array_ $eventsToMethodsArray
): void {
/** @var EventListenerTag[]|Tag[] $eventTags */
$eventTags = $methodNamesWithPriorities[0]->getTags();
foreach ($eventTags as $eventTag) {
if ($eventTag instanceof EventListenerTag && $eventTag->getEvent() === $eventName) {
$methodName = $eventTag->getMethod();
$priority = $eventTag->getPriority();
break;
}
}
if (! isset($methodName, $priority)) {
return;
}
if ($priority !== 0) {
$methodNameWithPriorityArray = new Array_();
$methodNameWithPriorityArray->items[] = new ArrayItem(new String_($methodName));
$methodNameWithPriorityArray->items[] = new ArrayItem(new LNumber((int) $priority));
$eventsToMethodsArray->items[] = new ArrayItem($methodNameWithPriorityArray, $expr);
} else {
$eventsToMethodsArray->items[] = new ArrayItem(new String_($methodName), $expr);
}
}
/**
* @param ClassConstFetch|String_ $expr
* @param ServiceDefinition[] $methodNamesWithPriorities
*/
private function createMultipleMethods(
array $methodNamesWithPriorities,
Expr $expr,
Array_ $eventsToMethodsArray,
string $eventName
): void {
$eventItems = [];
$alreadyUsedTags = [];
foreach ($methodNamesWithPriorities as $methodNamesWithPriority) {
foreach ($methodNamesWithPriority->getTags() as $tag) {
if (! $tag instanceof EventListenerTag) {
continue;
}
if ($this->shouldSkip($eventName, $tag, $alreadyUsedTags)) {
continue;
}
$eventItems[] = $this->createEventItem($tag);
$alreadyUsedTags[] = $tag;
}
}
$multipleMethodsArray = new Array_($eventItems);
$eventsToMethodsArray->items[] = new ArrayItem($multipleMethodsArray, $expr);
}
private function decorateClassMethodWithReturnType(ClassMethod $classMethod): void
{
if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) {
$classMethod->returnType = new Identifier('array');
}
$returnType = new ArrayType(new MixedType(), new MixedType(true));
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod);
$phpDocInfo->changeReturnType($returnType);
}
/**
* @param TagInterface[] $alreadyUsedTags
*/
private function shouldSkip(string $eventName, EventListenerTag $eventListenerTag, array $alreadyUsedTags): bool
{
if ($eventName !== $eventListenerTag->getEvent()) {
return true;
}
return in_array($eventListenerTag, $alreadyUsedTags, true);
}
private function createEventItem(EventListenerTag $eventListenerTag): ArrayItem
{
if ($eventListenerTag->getPriority() !== 0) {
$methodNameWithPriorityArray = new Array_();
$methodNameWithPriorityArray->items[] = new ArrayItem(new String_($eventListenerTag->getMethod()));
$methodNameWithPriorityArray->items[] = new ArrayItem(new LNumber($eventListenerTag->getPriority()));
return new ArrayItem($methodNameWithPriorityArray);
}
return new ArrayItem(new String_($eventListenerTag->getMethod()));
}
/**
* @param EventNameToClassAndConstant[] $eventNamesToClassConstants
* @return String_|ClassConstFetch
*/
private function createEventName(string $eventName, array $eventNamesToClassConstants): Node
{
if (class_exists($eventName)) {
return $this->nodeFactory->createClassConstReference($eventName);
}
// is string a that could be caught in constant, e.g. KernelEvents?
foreach ($eventNamesToClassConstants as $eventNameToClassConstant) {
if ($eventNameToClassConstant->getEventName() !== $eventName) {
continue;
}
return $this->nodeFactory->createClassConstFetch(
$eventNameToClassConstant->getEventClass(),
$eventNameToClassConstant->getEventConstant()
);
}
return new String_($eventName);
}
}

View File

@ -6,26 +6,13 @@ namespace Rector\SymfonyCodeQuality\Rector\Class_;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Type\ArrayType;
use PHPStan\Type\MixedType;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Symfony\Contract\Tag\TagInterface;
use Rector\Symfony\ServiceMapProvider;
use Rector\Symfony\ValueObject\ServiceDefinition;
use Rector\Symfony\ValueObject\Tag;
use Rector\Symfony\ValueObject\Tag\EventListenerTag;
use Rector\SymfonyCodeQuality\ApplicationMetadata\ListenerServiceDefinitionProvider;
use Rector\SymfonyCodeQuality\NodeFactory\GetSubscriberEventsClassMethodFactory;
use Rector\SymfonyCodeQuality\ValueObject\EventNameToClassAndConstant;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@ -56,36 +43,25 @@ final class EventListenerToEventSubscriberRector extends AbstractRector
*/
private const LISTENER_MATCH_REGEX = '#^(.*?)(Listener)?$#';
/**
* @var string
* @see https://regex101.com/r/j6SAga/1
*/
private const SYMFONY_FAMILY_REGEX = '#^(Symfony|Sensio|Doctrine)\b#';
/**
* @var bool
*/
private $areListenerClassesLoaded = false;
/**
* @var EventNameToClassAndConstant[]
*/
private $eventNamesToClassConstants = [];
/**
* @var ServiceDefinition[][][]
* @var ListenerServiceDefinitionProvider
*/
private $listenerClassesToEvents = [];
private $listenerServiceDefinitionProvider;
/**
* @var ServiceMapProvider
* @var GetSubscriberEventsClassMethodFactory
*/
private $applicationServiceMapProvider;
public function __construct(ServiceMapProvider $applicationServiceMapProvider)
{
$this->applicationServiceMapProvider = $applicationServiceMapProvider;
private $getSubscriberEventsClassMethodFactory;
public function __construct(
ListenerServiceDefinitionProvider $listenerServiceDefinitionProvider,
GetSubscriberEventsClassMethodFactory $getSubscriberEventsClassMethodFactory
) {
$this->eventNamesToClassConstants = [
// kernel events
new EventNameToClassAndConstant('kernel.request', self::KERNEL_EVENTS_CLASS, 'REQUEST'),
@ -105,6 +81,8 @@ final class EventListenerToEventSubscriberRector extends AbstractRector
new EventNameToClassAndConstant('console.terminate', self::CONSOLE_EVENTS_CLASS, 'TERMINATE'),
new EventNameToClassAndConstant('console.error', self::CONSOLE_EVENTS_CLASS, 'ERROR'),
];
$this->listenerServiceDefinitionProvider = $listenerServiceDefinitionProvider;
$this->getSubscriberEventsClassMethodFactory = $getSubscriberEventsClassMethodFactory;
}
public function getRuleDefinition(): RuleDefinition
@ -175,7 +153,7 @@ CODE_SAMPLE
}
// there must be event dispatcher in the application
$listenerClassesToEventsToMethods = $this->getListenerClassesToEventsToMethods();
$listenerClassesToEventsToMethods = $this->listenerServiceDefinitionProvider->extract();
if ($listenerClassesToEventsToMethods === []) {
return null;
}
@ -200,40 +178,7 @@ CODE_SAMPLE
}
/**
* @return ServiceDefinition[][][]
*/
private function getListenerClassesToEventsToMethods(): array
{
if ($this->areListenerClassesLoaded) {
return $this->listenerClassesToEvents;
}
$serviceMap = $this->applicationServiceMapProvider->provide();
$eventListeners = $serviceMap->getServicesByTag('kernel.event_listener');
foreach ($eventListeners as $eventListener) {
// skip Symfony core listeners
if (Strings::match((string) $eventListener->getClass(), self::SYMFONY_FAMILY_REGEX)) {
continue;
}
foreach ($eventListener->getTags() as $tag) {
if (! $tag instanceof EventListenerTag) {
continue;
}
$eventName = $tag->getEvent();
$this->listenerClassesToEvents[$eventListener->getClass()][$eventName][] = $eventListener;
}
}
$this->areListenerClassesLoaded = true;
return $this->listenerClassesToEvents;
}
/**
* @param mixed[] $eventsToMethods
* @param array<string, ServiceDefinition[]> $eventsToMethods
*/
private function changeListenerToSubscriberWithMethods(Class_ $class, array $eventsToMethods): Class_
{
@ -246,176 +191,12 @@ CODE_SAMPLE
$class->name = new Identifier($classShortName . 'EventSubscriber');
$classMethod = $this->createGetSubscribedEventsClassMethod($eventsToMethods);
$classMethod = $this->getSubscriberEventsClassMethodFactory->createFromEventsToMethods(
$eventsToMethods,
$this->eventNamesToClassConstants
);
$class->stmts[] = $classMethod;
return $class;
}
/**
* @param mixed[][] $eventsToMethods
*/
private function createGetSubscribedEventsClassMethod(array $eventsToMethods): ClassMethod
{
$getSubscribersClassMethod = $this->nodeFactory->createPublicMethod('getSubscribedEvents');
$eventsToMethodsArray = new Array_();
$this->makeStatic($getSubscribersClassMethod);
foreach ($eventsToMethods as $eventName => $methodNamesWithPriorities) {
$eventNameExpr = $this->createEventName($eventName);
if (count($methodNamesWithPriorities) === 1) {
$this->createSingleMethod(
$methodNamesWithPriorities,
$eventName,
$eventNameExpr,
$eventsToMethodsArray
);
} else {
$this->createMultipleMethods(
$methodNamesWithPriorities,
$eventNameExpr,
$eventsToMethodsArray,
$eventName
);
}
}
$getSubscribersClassMethod->stmts[] = new Return_($eventsToMethodsArray);
$this->decorateClassMethodWithReturnType($getSubscribersClassMethod);
return $getSubscribersClassMethod;
}
/**
* @return String_|ClassConstFetch
*/
private function createEventName(string $eventName): Node
{
if (class_exists($eventName)) {
return $this->createClassConstReference($eventName);
}
// is string a that could be caught in constant, e.g. KernelEvents?
foreach ($this->eventNamesToClassConstants as $eventNameToClassConstant) {
if ($eventNameToClassConstant->getEventName() !== $eventName) {
continue;
}
return $this->createClassConstFetch(
$eventNameToClassConstant->getEventClass(),
$eventNameToClassConstant->getEventConstant()
);
}
return new String_($eventName);
}
/**
* @param ClassConstFetch|String_ $expr
* @param ServiceDefinition[] $methodNamesWithPriorities
*/
private function createSingleMethod(
array $methodNamesWithPriorities,
string $eventName,
Expr $expr,
Array_ $eventsToMethodsArray
): void {
/** @var EventListenerTag[]|Tag[] $eventTags */
$eventTags = $methodNamesWithPriorities[0]->getTags();
foreach ($eventTags as $eventTag) {
if ($eventTag instanceof EventListenerTag && $eventTag->getEvent() === $eventName) {
$methodName = $eventTag->getMethod();
$priority = $eventTag->getPriority();
break;
}
}
if (! isset($methodName, $priority)) {
return;
}
if ($priority !== 0) {
$methodNameWithPriorityArray = new Array_();
$methodNameWithPriorityArray->items[] = new ArrayItem(new String_($methodName));
$methodNameWithPriorityArray->items[] = new ArrayItem(new LNumber((int) $priority));
$eventsToMethodsArray->items[] = new ArrayItem($methodNameWithPriorityArray, $expr);
} else {
$eventsToMethodsArray->items[] = new ArrayItem(new String_($methodName), $expr);
}
}
/**
* @param ClassConstFetch|String_ $expr
* @param ServiceDefinition[] $methodNamesWithPriorities
*/
private function createMultipleMethods(
array $methodNamesWithPriorities,
Expr $expr,
Array_ $eventsToMethodsArray,
string $eventName
): void {
$eventItems = [];
$alreadyUsedTags = [];
foreach ($methodNamesWithPriorities as $methodNamesWithPriority) {
foreach ($methodNamesWithPriority->getTags() as $tag) {
if (! $tag instanceof EventListenerTag) {
continue;
}
if ($this->shouldSkip($eventName, $tag, $alreadyUsedTags)) {
continue;
}
$eventItems[] = $this->createEventItem($tag);
$alreadyUsedTags[] = $tag;
}
}
$multipleMethodsArray = new Array_($eventItems);
$eventsToMethodsArray->items[] = new ArrayItem($multipleMethodsArray, $expr);
}
private function decorateClassMethodWithReturnType(ClassMethod $classMethod): void
{
if ($this->isAtLeastPhpVersion(PhpVersionFeature::SCALAR_TYPES)) {
$classMethod->returnType = new Identifier('array');
}
$returnType = new ArrayType(new MixedType(), new MixedType(true));
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod);
$phpDocInfo->changeReturnType($returnType);
}
/**
* @param TagInterface[] $alreadyUsedTags
*/
private function shouldSkip(string $eventName, EventListenerTag $eventListenerTag, array $alreadyUsedTags): bool
{
if ($eventName !== $eventListenerTag->getEvent()) {
return true;
}
return in_array($eventListenerTag, $alreadyUsedTags, true);
}
private function createEventItem(EventListenerTag $eventListenerTag): ArrayItem
{
if ($eventListenerTag->getPriority() !== 0) {
$methodNameWithPriorityArray = new Array_();
$methodNameWithPriorityArray->items[] = new ArrayItem(new String_($eventListenerTag->getMethod()));
$methodNameWithPriorityArray->items[] = new ArrayItem(new LNumber($eventListenerTag->getPriority()));
return new ArrayItem($methodNameWithPriorityArray);
}
return new ArrayItem(new String_($eventListenerTag->getMethod()));
}
}

View File

@ -462,15 +462,15 @@ final class NodeFactory
return $uses;
}
public function createStaticCall(string $class, string $method): StaticCall
/**
* @param Node[] $args
*/
public function createStaticCall(string $class, string $method, array $args = []): StaticCall
{
if (in_array($class, self::REFERENCES, true)) {
$class = new Name($class);
} else {
$class = new FullyQualified($class);
}
return new StaticCall($class, $method);
$class = $this->createClassPart($class);
$staticCall = new StaticCall($class, $method);
$staticCall->args = $this->createArgs($args);
return $staticCall;
}
/**
@ -635,4 +635,13 @@ final class NodeFactory
return $arrayItem;
}
private function createClassPart(string $class): Name
{
if (in_array($class, self::REFERENCES, true)) {
return new Name($class);
}
return new FullyQualified($class);
}
}

View File

@ -92,7 +92,7 @@ services:
arguments:
maxClassCognitiveComplexity: 50
limitsByTypes:
Rector\Core\Rector\AbstractRector: 40
Rector\Core\Rector\AbstractRector: 35
Symfony\Component\Console\Command\Command: 40
PHPStan\Rule\Rule: 30