Skip to content

Commit

Permalink
Add ServiceSettersToSettersAutodiscoveryRector
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Feb 7, 2023
1 parent 948357f commit 1b584d0
Show file tree
Hide file tree
Showing 14 changed files with 390 additions and 35 deletions.
8 changes: 0 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,4 @@ composer.lock
# often customized locally - example on Github is just fine
rector-recipe.php

# testing
abz

# scoped & downgraded version
php-scoper.phar
box.phar
php-parallel-lint

tmp
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"require": {
"php": ">=8.1",
"ext-xml": "*",
"symfony/string": "^6.1"
"symfony/string": "^6.1",
"triun/longest-common-substring": "^1.0"
},
"require-dev": {
"phpstan/extension-installer": "^1.2",
Expand Down Expand Up @@ -46,7 +47,6 @@
]
},
"scripts": {
"release": "vendor/bin/monorepo-builder release patch --ansi",
"phpstan": "vendor/bin/phpstan analyse --ansi --error-format symplify",
"check-cs": "vendor/bin/ecs check --ansi",
"fix-cs": "vendor/bin/ecs check --fix --ansi",
Expand Down
37 changes: 34 additions & 3 deletions docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 75 Rules Overview
# 76 Rules Overview

## ActionSuffixRemoverRector

Expand Down Expand Up @@ -193,7 +193,10 @@ use Rector\Config\RectorConfig;
use Rector\Symfony\Rector\Class_\ChangeFileLoaderInExtensionAndKernelRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->ruleWithConfiguration(ChangeFileLoaderInExtensionAndKernelRector::class, [ChangeFileLoaderInExtensionAndKernelRector::FROM => 'xml', ChangeFileLoaderInExtensionAndKernelRector::TO => 'yaml']);
$rectorConfig->ruleWithConfiguration(ChangeFileLoaderInExtensionAndKernelRector::class, [
ChangeFileLoaderInExtensionAndKernelRector::FROM => 'xml',
ChangeFileLoaderInExtensionAndKernelRector::TO => 'yaml',
]);
};
```

Expand Down Expand Up @@ -1345,7 +1348,10 @@ use Rector\Symfony\Rector\FuncCall\ReplaceServiceArgumentRector;
use Rector\Symfony\ValueObject\ReplaceServiceArgument;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->ruleWithConfiguration(ReplaceServiceArgumentRector::class, [new ReplaceServiceArgument('ContainerInterface', new String_('service_container', []))]);
$rectorConfig->ruleWithConfiguration(ReplaceServiceArgumentRector::class, [
new ReplaceServiceArgument('ContainerInterface', new String_('service_container', [
])),
]);
};
```

Expand Down Expand Up @@ -1476,6 +1482,31 @@ Change `$service->set()` string names to class-type-based names, to allow `$cont

<br>

## ServiceSettersToSettersAutodiscoveryRector

Change `$services->set(...,` ...) to `$services->load(...,` ...) where meaning ful

- class: [`Rector\Symfony\Rector\Closure\ServiceSettersToSettersAutodiscoveryRector`](../src/Rector/Closure/ServiceSettersToSettersAutodiscoveryRector.php)

```diff
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

-use App\Services\FistService;
-use App\Services\SecondService;
-
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();

$services = $containerConfigurator->services();

- $services->set(FistService::class);
- $services->set(SecondService::class);
+ $services->load('App\\Services\\', '../src/Services/*');
};
```

<br>

## ServicesSetNameToSetTypeRector

Change `$services->set("name_type",` SomeType::class) to bare type, useful since Symfony 3.4
Expand Down
15 changes: 0 additions & 15 deletions monorepo-builder.php

This file was deleted.

8 changes: 2 additions & 6 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
parameters:
level: 8

reportUnmatchedIgnoredErrors: false

paths:
- config
- src
Expand Down Expand Up @@ -30,8 +32,6 @@ parameters:
- *Source/*
- */tests/*/Fixture*/Expected/*

reportUnmatchedIgnoredErrors: false

ignoreErrors:
-
message: '#Instead of "instanceof/is_a\(\)" use ReflectionProvider service or "\(new ObjectType\(<desired_type\>\)\)\-\>isSuperTypeOf\(<element_type\>\)" for static reflection to work#'
Expand Down Expand Up @@ -60,7 +60,3 @@ parameters:
-
message: '#Parameter \#2 \$name of method Rector\\Doctrine\\NodeAnalyzer\\AttrinationFinder\:\:hasByOne\(\) expects class\-string, string given#'
path: src/Rector/ClassMethod/ResponseReturnTypeControllerActionRector.php

# keep for BC
- '#Class "Rector\\Symfony\\Rector\\ClassMethod\\ConsoleExecuteReturnIntRector" has invalid namespace category "ClassMethod"\. Pick one of\: "Class_"#'
- '#Class "Rector\\Symfony\\Rector\\Closure\\ContainerGetNameToTypeInTestsRector" has invalid namespace category "Closure"\. Pick one of\: "MethodCall"#'
3 changes: 2 additions & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Naming\Rector\Foreach_\RenameForeachValueVariableToMatchMethodCallReturnTypeRector;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
Expand All @@ -22,7 +23,7 @@
'*/tests/*/Fixture*/Expected/*',
StringClassNameToClassConstantRector::class => [__DIR__ . '/config'],

\Rector\Naming\Rector\Foreach_\RenameForeachValueVariableToMatchMethodCallReturnTypeRector::class => [
RenameForeachValueVariableToMatchMethodCallReturnTypeRector::class => [
// "data" => "datum" false positive
__DIR__ . '/src/Rector/ClassMethod/AddRouteAnnotationRector.php',
],
Expand Down
223 changes: 223 additions & 0 deletions src/Rector/Closure/ServiceSettersToSettersAutodiscoveryRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<?php

declare(strict_types=1);

namespace Rector\Symfony\Rector\Closure;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ObjectType;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Rector\AbstractRector;
use Rector\Symfony\NodeAnalyzer\SymfonyPhpClosureDetector;
use Rector\Symfony\ValueObject\ClassNameAndFilePath;
use Symfony\Component\Filesystem\Filesystem;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Triun\LongestCommonSubstring\Solver;

/**
* @see \Rector\Symfony\Tests\Rector\Closure\ServiceSettersToSettersAutodiscoveryRector\ServiceSettersToSettersAutodiscoveryRectorTest
*/
final class ServiceSettersToSettersAutodiscoveryRector extends AbstractRector
{
private readonly Solver $solver;

public function __construct(
private readonly SymfonyPhpClosureDetector $symfonyPhpClosureDetector,
private readonly ReflectionProvider $reflectionProvider,
private readonly Filesystem $filesystem,
) {
$this->solver = new Solver();
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Change $services->set(..., ...) to $services->load(..., ...) where meaning ful', [
new CodeSample(
<<<'CODE_SAMPLE'
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use App\Services\FistService;
use App\Services\SecondService;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$services = $containerConfigurator->services();
$services->set(FistService::class);
$services->set(SecondService::class);
};
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$services = $containerConfigurator->services();
$services->load('App\\Services\\', '../src/Services/*');
};
CODE_SAMPLE
),
]);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Closure::class];
}

/**
* @param Closure $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->symfonyPhpClosureDetector->detect($node)) {
return null;
}

$bareServicesSetMethodCalls = $this->collectServiceSetMethodCalls($node);
if ($bareServicesSetMethodCalls === []) {
return null;
}

$classNamesAndFilesPaths = $this->createClassNamesAndFilePaths($bareServicesSetMethodCalls);

$classNames = array_map(
fn (ClassNameAndFilePath $classNameAndFilePath) => $classNameAndFilePath->getClassName(),
$classNamesAndFilesPaths
);

$sharedNamespace = $this->solver->solve(...$classNames);
if (! is_string($sharedNamespace)) {
return null;
}

$firstClassNameAndFilePath = $classNamesAndFilesPaths[0];
$classFilePath = $firstClassNameAndFilePath->getFilePath();

$relativeDirectoryPath = $this->filesystem->makePathRelative(
dirname($classFilePath),
dirname($this->file->getFilePath())
);

$directoryConcat = new Concat(
new ConstFetch(new Name('__DIR__')),
new String_('/' . $relativeDirectoryPath)
);
$args = [new Arg(new String_($sharedNamespace)), new Arg($directoryConcat)];
$loadMethodCall = new MethodCall(new Variable('services'), 'load', $args);
$node->stmts[] = new Expression($loadMethodCall);

// remove all method calls
foreach ($bareServicesSetMethodCalls as $bareServiceSetMethodCall) {
$this->removeNode($bareServiceSetMethodCall);
}

return $node;
}

public function isBareServicesSetMethodCall(MethodCall $methodCall): bool
{
if (! $this->isObjectType(
$methodCall->var,
new ObjectType('Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator')
)) {
return false;
}

if (! $this->isName($methodCall->name, 'set')) {
return false;
}

// must have exactly single argument
if (count($methodCall->getArgs()) !== 1) {
return false;
}

$firstArg = $methodCall->getArgs()[0];

// first argument must be a class name, e.g. SomeClass::class
return $firstArg->value instanceof ClassConstFetch;
}

/**
* @return MethodCall[]
*/
private function collectServiceSetMethodCalls(Closure $closure): array
{
$servicesSetMethodCalls = [];

$this->traverseNodesWithCallable($closure, function (Node $node) use (&$servicesSetMethodCalls) {
if (! $node instanceof Expression) {
return null;
}

if (! $node->expr instanceof MethodCall) {
return null;
}

$methodCall = $node->expr;
if (! $this->isBareServicesSetMethodCall($methodCall)) {
return null;
}

$servicesSetMethodCalls[] = $methodCall;

return null;
});

return $servicesSetMethodCalls;
}

/**
* @param MethodCall[] $methodsCalls
* @return ClassNameAndFilePath[]
*/
private function createClassNamesAndFilePaths(array $methodsCalls): array
{
$classNamesAndFilesPaths = [];

foreach ($methodsCalls as $methodCall) {
$firstArg = $methodCall->getArgs()[0];
$serviceClassReference = $this->valueResolver->getValue($firstArg->value);

if (! is_string($serviceClassReference)) {
throw new ShouldNotHappenException();
}

$classReflection = $this->reflectionProvider->getClass($serviceClassReference);

// we only work with known local classes
if ($classReflection->isInternal()) {
continue;
}

$filename = $classReflection->getFileName();
if (! is_string($filename)) {
continue;
}

$classNamesAndFilesPaths[] = new ClassNameAndFilePath($classReflection->getName(), $filename);
}

return $classNamesAndFilesPaths;
}
}

0 comments on commit 1b584d0

Please sign in to comment.