Skip to content

Commit e9741dc

Browse files
committed
[config] Add RemoveConstructorAutowireServiceRector
1 parent 6337a1b commit e9741dc

File tree

12 files changed

+405
-9
lines changed

12 files changed

+405
-9
lines changed

config/sets/symfony/configs.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Rector\Config\RectorConfig;
66
use Rector\Symfony\Configs\Rector\Closure\MergeServiceNameTypeRector;
7+
use Rector\Symfony\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector;
78
use Rector\Symfony\Configs\Rector\Closure\ServiceArgsToServiceNamedArgRector;
89
use Rector\Symfony\Configs\Rector\Closure\ServiceSetStringNameToClassNameRector;
910
use Rector\Symfony\Configs\Rector\Closure\ServiceSettersToSettersAutodiscoveryRector;
@@ -16,5 +17,6 @@
1617
ServiceSetStringNameToClassNameRector::class,
1718
ServiceSettersToSettersAutodiscoveryRector::class,
1819
ServiceTagsToDefaultsAutoconfigureRector::class,
20+
RemoveConstructorAutowireServiceRector::class,
1921
]);
2022
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\Fixture;
6+
7+
use Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\Source\AnotherClassWithoutConstructor;
8+
use Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\Source\PassedAsDependency;
9+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
10+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
11+
12+
return static function (ContainerConfigurator $containerConfigurator): void {
13+
$services = $containerConfigurator->services();
14+
15+
$services->set(AnotherClassWithoutConstructor::class)
16+
->arg('$passedAsDependency', service(PassedAsDependency::class));
17+
};
18+
19+
?>
20+
-----
21+
<?php
22+
23+
declare(strict_types=1);
24+
25+
namespace Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\Fixture;
26+
27+
use Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\Source\AnotherClassWithoutConstructor;
28+
use Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\Source\PassedAsDependency;
29+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
30+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
31+
32+
return static function (ContainerConfigurator $containerConfigurator): void {
33+
$services = $containerConfigurator->services();
34+
35+
$services->set(AnotherClassWithoutConstructor::class);
36+
};
37+
38+
?>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class RemoveConstructorAutowireServiceRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\Source;
4+
5+
final class AnotherClassWithoutConstructor
6+
{
7+
public function __construct(
8+
PassedAsDependency $passedAsDependency
9+
) {
10+
}
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\Source;
4+
5+
final class PassedAsDependency
6+
{
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Symfony\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(RemoveConstructorAutowireServiceRector::class);
10+
};
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Configs\Rector\Closure;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Expr\ClassConstFetch;
10+
use PhpParser\Node\Expr\Closure;
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PhpParser\Node\Expr\MethodCall;
13+
use PhpParser\Node\Scalar\String_;
14+
use PHPStan\Type\ObjectType;
15+
use Rector\PhpParser\Node\Value\ValueResolver;
16+
use Rector\Rector\AbstractRector;
17+
use Rector\Symfony\Configs\Rector\Reflection\ConstructorReflectionTypesResolver;
18+
use Rector\Symfony\Enum\SymfonyClass;
19+
use Rector\Symfony\Enum\SymfonyFunction;
20+
use Rector\Symfony\NodeAnalyzer\SymfonyPhpClosureDetector;
21+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
22+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
23+
24+
/**
25+
* @see \Rector\Symfony\Tests\Configs\Rector\Closure\RemoveConstructorAutowireServiceRector\RemoveConstructorAutowireServiceRectorTest
26+
*/
27+
final class RemoveConstructorAutowireServiceRector extends AbstractRector
28+
{
29+
private bool $hasChanged = false;
30+
31+
public function __construct(
32+
private readonly SymfonyPhpClosureDetector $symfonyPhpClosureDetector,
33+
private readonly ValueResolver $valueResolver,
34+
private readonly ConstructorReflectionTypesResolver $constructorReflectionTypesResolver,
35+
) {
36+
}
37+
38+
public function getRuleDefinition(): RuleDefinition
39+
{
40+
return new RuleDefinition(
41+
'Remove service that is passed as arg, but already autowired via constructor',
42+
[
43+
new CodeSample(
44+
<<<'CODE_SAMPLE'
45+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
46+
47+
return static function (ContainerConfigurator $containerConfigurator): void {
48+
$services = $containerConfigurator->services();
49+
50+
$services->set(\App\SomeClass::class)
51+
->arg('$someService', ref(\App\SomeService::class));
52+
};
53+
54+
final class SomeClass
55+
{
56+
public function __construct(private SomeService $someService)
57+
{
58+
}
59+
}
60+
CODE_SAMPLE
61+
62+
,
63+
<<<'CODE_SAMPLE'
64+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
65+
66+
return static function (ContainerConfigurator $containerConfigurator): void {
67+
$services = $containerConfigurator->services();
68+
69+
$services->set(\App\SomeClass::class);
70+
};
71+
72+
final class SomeClass
73+
{
74+
public function __construct(private SomeService $someService)
75+
{
76+
}
77+
}
78+
CODE_SAMPLE
79+
),
80+
81+
]
82+
);
83+
}
84+
85+
/**
86+
* @return array<class-string<Node>>
87+
*/
88+
public function getNodeTypes(): array
89+
{
90+
return [Closure::class];
91+
}
92+
93+
/**
94+
* @param Closure $node
95+
*/
96+
public function refactor(Node $node): ?Node
97+
{
98+
$this->hasChanged = false;
99+
100+
if (! $this->symfonyPhpClosureDetector->detect($node)) {
101+
return null;
102+
}
103+
104+
$this->handleSetServices($node);
105+
106+
if ($this->hasChanged) {
107+
return $node;
108+
}
109+
110+
return null;
111+
}
112+
113+
private function handleSetServices(Closure $closure): void
114+
{
115+
$this->traverseNodesWithCallable($closure->stmts, function (Node $node): ?Expr {
116+
if (! $node instanceof MethodCall) {
117+
return null;
118+
}
119+
120+
if (! $this->isName($node->name, 'arg')) {
121+
return null;
122+
}
123+
124+
$serviceClass = $this->matchSetServicesClass($node);
125+
if (! is_string($serviceClass)) {
126+
return null;
127+
}
128+
129+
$constructorTypesByParameterName = $this->constructorReflectionTypesResolver->resolve($serviceClass);
130+
if ($constructorTypesByParameterName === null) {
131+
return null;
132+
}
133+
134+
$argName = $node->getArgs()[0]
135+
->value;
136+
$serviceArgExpr = $node->getArgs()[1]
137+
->value;
138+
139+
if (! $argName instanceof String_) {
140+
return null;
141+
}
142+
143+
$bareParameterName = ltrim($argName->value, '$');
144+
$knownParameterType = $constructorTypesByParameterName[$bareParameterName] ?? null;
145+
if (! $knownParameterType instanceof ObjectType) {
146+
return null;
147+
}
148+
149+
if (! $this->isParameterTypeMatchingPassedArgExprClass($serviceArgExpr, $knownParameterType)) {
150+
return null;
151+
}
152+
153+
return $node->var;
154+
155+
// return NodeVisitor::REMOVE_NODE;
156+
});
157+
}
158+
159+
private function isSetServices(MethodCall $methodCall): bool
160+
{
161+
if (! $this->isName($methodCall->name, 'set')) {
162+
return false;
163+
}
164+
165+
return $this->isObjectType($methodCall->var, new ObjectType(SymfonyClass::SERVICE_CONFIGURATOR));
166+
}
167+
168+
// /**
169+
// * @return array<string, \PHPStan\Type\Type>|null
170+
// */
171+
// private function resolveConstructorTypesByParameterName(string $serviceClass): ?array
172+
// {
173+
// if (! $this->reflectionProvider->hasClass($serviceClass)) {
174+
// return null;
175+
// }
176+
//
177+
// $constructorReflection = $this->reflectionResolver->resolveMethodReflection(
178+
// $serviceClass,
179+
// MethodName::CONSTRUCT,
180+
// null
181+
// );
182+
//
183+
// if (! $constructorReflection instanceof PhpMethodReflection) {
184+
// return null;
185+
// }
186+
//
187+
// return $this->resolveMethodReflectionParameterTypes($constructorReflection);
188+
// }
189+
190+
private function matchSetServicesClass(MethodCall $methodCall): ?string
191+
{
192+
while ($methodCall instanceof MethodCall) {
193+
if ($this->isSetServices($methodCall)) {
194+
break;
195+
}
196+
197+
$methodCall = $methodCall->var;
198+
}
199+
200+
/** @var MethodCall $methodCall */
201+
$firstArg = $methodCall->getArgs()[0];
202+
if (! $firstArg->value instanceof ClassConstFetch) {
203+
return null;
204+
}
205+
206+
return $this->valueResolver->getValue($firstArg->value);
207+
}
208+
209+
private function isParameterTypeMatchingPassedArgExprClass(
210+
Expr $serviceArgExpr,
211+
ObjectType $objectType
212+
): bool {
213+
if (! $serviceArgExpr instanceof FuncCall) {
214+
return false;
215+
}
216+
217+
if (! $this->isName($serviceArgExpr->name, SymfonyFunction::SERVICE)) {
218+
return false;
219+
}
220+
221+
$dependencyServiceExpr = $serviceArgExpr->getArgs()[0]
222+
->value;
223+
$dependencyService = $this->valueResolver->getValue($dependencyServiceExpr);
224+
225+
return $dependencyService === $objectType->getClassName();
226+
}
227+
}

rules/Configs/Rector/Closure/ServiceArgsToServiceNamedArgRector.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
use PhpParser\Node\Expr\MethodCall;
1515
use PhpParser\Node\Scalar\String_;
1616
use PHPStan\Reflection\ParametersAcceptorSelector;
17-
use PHPStan\Reflection\Php\PhpParameterReflection;
1817
use PHPStan\Reflection\ReflectionProvider;
1918
use PHPStan\Type\ObjectType;
2019
use Rector\PhpParser\Node\Value\ValueResolver;
2120
use Rector\Rector\AbstractRector;
21+
use Rector\Symfony\Enum\SymfonyClass;
2222
use Rector\Symfony\NodeAnalyzer\SymfonyPhpClosureDetector;
2323
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
2424
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
@@ -162,9 +162,8 @@ private function resolveConstructorParameterNames(string $serviceClass): array
162162
$extendedMethodReflection->getVariants()
163163
);
164164

165-
foreach ($extendedParametersAcceptor->getParameters() as $parameterReflectionWithPhpDoc) {
166-
/** @var PhpParameterReflection $parameterReflectionWithPhpDoc */
167-
$constructorParameterNames[] = '$' . $parameterReflectionWithPhpDoc->getName();
165+
foreach ($extendedParametersAcceptor->getParameters() as $extendedParameterReflection) {
166+
$constructorParameterNames[] = '$' . $extendedParameterReflection->getName();
168167
}
169168

170169
return $constructorParameterNames;
@@ -176,10 +175,7 @@ private function isServiceArgsMethodCall(MethodCall $methodCall): bool
176175
return false;
177176
}
178177

179-
return $this->isObjectType(
180-
$methodCall->var,
181-
new ObjectType('Symfony\Component\DependencyInjection\Loader\Configurator\ServiceConfigurator')
182-
);
178+
return $this->isObjectType($methodCall->var, new ObjectType(SymfonyClass::SERVICE_CONFIGURATOR));
183179
}
184180

185181
private function createArgMethodCall(

0 commit comments

Comments
 (0)