Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ServiceSettersToSettersAutodiscoveryRector #343

Merged
merged 1 commit into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}