Skip to content

Commit 947b9c7

Browse files
authored
[symfony] add NoSetClassServiceDuplicationRule (#237)
1 parent ce6cf97 commit 947b9c7

File tree

8 files changed

+209
-0
lines changed

8 files changed

+209
-0
lines changed

config/symfony-config-rules.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ rules:
1616

1717
# $services->set('X', 'X')
1818
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoServiceSameNameSetClassRule
19+
20+
# $services->set('X')->class('X')
21+
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule

src/Enum/RuleIdentifier/SymfonyRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,6 @@ final class SymfonyRuleIdentifier
5959
public const PREFER_AUTOWIRE_ATTRIBUTE_OVER_CONFIG_PARAM = 'symfony.preferAutowireAttributeOverConfigParam';
6060

6161
public const RULE_IDENTIFIER = 'symfony.noServiceAutowireDuplicate';
62+
63+
public const NO_SET_CLASS_SERVICE_DUPLICATE = 'symfony.noSetClassServiceDuplicate';
6264
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Symfony\ConfigClosure;
6+
7+
use Nette\Utils\Strings;
8+
use PhpParser\Node;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Identifier;
11+
use PhpParser\PrettyPrinter\Standard;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Rules\Rule;
14+
use PHPStan\Rules\RuleError;
15+
use PHPStan\Rules\RuleErrorBuilder;
16+
use Symplify\PHPStanRules\Enum\RuleIdentifier\SymfonyRuleIdentifier;
17+
18+
/**
19+
* @implements Rule<MethodCall>
20+
*/
21+
final readonly class NoSetClassServiceDuplicationRule implements Rule
22+
{
23+
/**
24+
* @var string
25+
*/
26+
public const ERROR_MESSAGE = 'Instead of "$services->set(%s)->class(%s)" that brings no value, use simple $services->set(%s)';
27+
28+
private Standard $standard;
29+
30+
public function __construct()
31+
{
32+
$this->standard = new Standard();
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return MethodCall::class;
38+
}
39+
40+
/**
41+
* @param MethodCall $node
42+
* @return RuleError[]
43+
*/
44+
public function processNode(Node $node, Scope $scope): array
45+
{
46+
if ($node->isFirstClassCallable()) {
47+
return [];
48+
}
49+
50+
// parent method must be a method call too
51+
if (! $node->var instanceof MethodCall) {
52+
return [];
53+
}
54+
55+
if (! $this->isMethodName($node->name, 'class')) {
56+
return [];
57+
}
58+
59+
$parentMethodCall = $node->var;
60+
if (! $this->isMethodName($parentMethodCall->name, 'set')) {
61+
return [];
62+
}
63+
64+
$parentSoleArgContents = $this->resolveSoleArgContents($parentMethodCall);
65+
if ($parentSoleArgContents === null) {
66+
return [];
67+
}
68+
69+
$currentSoleArgContents = $this->resolveSoleArgContents($node);
70+
if ($currentSoleArgContents === null) {
71+
return [];
72+
}
73+
74+
if ($parentSoleArgContents !== $currentSoleArgContents) {
75+
return [];
76+
}
77+
78+
if (str_contains($parentSoleArgContents, '\\')) {
79+
$shortClassName = Strings::after($parentSoleArgContents, '\\', -1);
80+
} else {
81+
$shortClassName = $parentSoleArgContents;
82+
}
83+
84+
$errorMessage = sprintf(
85+
self::ERROR_MESSAGE,
86+
$shortClassName,
87+
$shortClassName,
88+
$shortClassName
89+
);
90+
91+
$identifierRuleError = RuleErrorBuilder::message($errorMessage)
92+
->identifier(SymfonyRuleIdentifier::NO_SET_CLASS_SERVICE_DUPLICATE)
93+
->build();
94+
95+
return [$identifierRuleError];
96+
}
97+
98+
private function isMethodName(Node $node, string $name): bool
99+
{
100+
if (! $node instanceof Identifier) {
101+
return false;
102+
}
103+
104+
return $node->toString() === $name;
105+
}
106+
107+
private function resolveSoleArgContents(MethodCall $methodCall): ?string
108+
{
109+
if (count($methodCall->getArgs()) !== 1) {
110+
return null;
111+
}
112+
113+
$firstArgExpr = $methodCall->getArgs()[0]
114+
->value;
115+
return $this->standard->prettyPrintExpr($firstArgExpr);
116+
}
117+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\ServicesExcludedDirectoryMustExistRule\Fixture;
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule\Source\SomeClassToBeSet;
7+
8+
return function (ContainerConfigurator $container) {
9+
$services = $container->services();
10+
11+
$services->set(SomeClassToBeSet::class)
12+
->class(SomeClassToBeSet::class);
13+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\ServicesExcludedDirectoryMustExistRule\Fixture;
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule\Source\SomeClassToBeSet;
7+
8+
return function (ContainerConfigurator $container) {
9+
$services = $container->services();
10+
11+
$services->set(SomeClassToBeSet::class)
12+
->class('AnotherValue');
13+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\ServicesExcludedDirectoryMustExistRule\Fixture;
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule\Source\SomeClassToBeSet;
7+
8+
return function (ContainerConfigurator $container) {
9+
$services = $container->services();
10+
11+
$services->set(SomeClassToBeSet::class);
12+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule;
12+
13+
final class NoSetClassServiceDuplicationRuleTest extends RuleTestCase
14+
{
15+
/**
16+
* @param mixed[] $expectedErrorMessagesWithLines
17+
*/
18+
#[DataProvider('provideData')]
19+
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
20+
{
21+
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
22+
}
23+
24+
public static function provideData(): Iterator
25+
{
26+
yield [__DIR__ . '/Fixture/SetAndClassConfig.php', [
27+
[
28+
sprintf(NoSetClassServiceDuplicationRule::ERROR_MESSAGE, 'SomeClassToBeSet::class', 'SomeClassToBeSet::class', 'SomeClassToBeSet::class'),
29+
11,
30+
],
31+
]];
32+
33+
yield [__DIR__ . '/Fixture/SkipDifferentSetAndClass.php', []];
34+
yield [__DIR__ . '/Fixture/SkipSoleSet.php', []];
35+
}
36+
37+
protected function getRule(): Rule
38+
{
39+
return new NoSetClassServiceDuplicationRule();
40+
}
41+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule\Source;
4+
5+
final class SomeClassToBeSet
6+
{
7+
8+
}

0 commit comments

Comments
 (0)