diff --git a/config/symfony-config-rules.neon b/config/symfony-config-rules.neon index 7bb57946..a6658320 100644 --- a/config/symfony-config-rules.neon +++ b/config/symfony-config-rules.neon @@ -16,3 +16,6 @@ rules: # $services->set('X', 'X') - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoServiceSameNameSetClassRule + + # $services->set('X')->class('X') + - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule diff --git a/src/Enum/RuleIdentifier/SymfonyRuleIdentifier.php b/src/Enum/RuleIdentifier/SymfonyRuleIdentifier.php index d394bb1b..6b3bbb1f 100644 --- a/src/Enum/RuleIdentifier/SymfonyRuleIdentifier.php +++ b/src/Enum/RuleIdentifier/SymfonyRuleIdentifier.php @@ -59,4 +59,6 @@ final class SymfonyRuleIdentifier public const PREFER_AUTOWIRE_ATTRIBUTE_OVER_CONFIG_PARAM = 'symfony.preferAutowireAttributeOverConfigParam'; public const RULE_IDENTIFIER = 'symfony.noServiceAutowireDuplicate'; + + public const NO_SET_CLASS_SERVICE_DUPLICATE = 'symfony.noSetClassServiceDuplicate'; } diff --git a/src/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule.php b/src/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule.php new file mode 100644 index 00000000..ee006888 --- /dev/null +++ b/src/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule.php @@ -0,0 +1,117 @@ + + */ +final readonly class NoSetClassServiceDuplicationRule implements Rule +{ + /** + * @var string + */ + public const ERROR_MESSAGE = 'Instead of "$services->set(%s)->class(%s)" that brings no value, use simple $services->set(%s)'; + + private Standard $standard; + + public function __construct() + { + $this->standard = new Standard(); + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + /** + * @param MethodCall $node + * @return RuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + if ($node->isFirstClassCallable()) { + return []; + } + + // parent method must be a method call too + if (! $node->var instanceof MethodCall) { + return []; + } + + if (! $this->isMethodName($node->name, 'class')) { + return []; + } + + $parentMethodCall = $node->var; + if (! $this->isMethodName($parentMethodCall->name, 'set')) { + return []; + } + + $parentSoleArgContents = $this->resolveSoleArgContents($parentMethodCall); + if ($parentSoleArgContents === null) { + return []; + } + + $currentSoleArgContents = $this->resolveSoleArgContents($node); + if ($currentSoleArgContents === null) { + return []; + } + + if ($parentSoleArgContents !== $currentSoleArgContents) { + return []; + } + + if (str_contains($parentSoleArgContents, '\\')) { + $shortClassName = Strings::after($parentSoleArgContents, '\\', -1); + } else { + $shortClassName = $parentSoleArgContents; + } + + $errorMessage = sprintf( + self::ERROR_MESSAGE, + $shortClassName, + $shortClassName, + $shortClassName + ); + + $identifierRuleError = RuleErrorBuilder::message($errorMessage) + ->identifier(SymfonyRuleIdentifier::NO_SET_CLASS_SERVICE_DUPLICATE) + ->build(); + + return [$identifierRuleError]; + } + + private function isMethodName(Node $node, string $name): bool + { + if (! $node instanceof Identifier) { + return false; + } + + return $node->toString() === $name; + } + + private function resolveSoleArgContents(MethodCall $methodCall): ?string + { + if (count($methodCall->getArgs()) !== 1) { + return null; + } + + $firstArgExpr = $methodCall->getArgs()[0] + ->value; + return $this->standard->prettyPrintExpr($firstArgExpr); + } +} diff --git a/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SetAndClassConfig.php b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SetAndClassConfig.php new file mode 100644 index 00000000..5828f6ce --- /dev/null +++ b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SetAndClassConfig.php @@ -0,0 +1,13 @@ +services(); + + $services->set(SomeClassToBeSet::class) + ->class(SomeClassToBeSet::class); +}; diff --git a/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SkipDifferentSetAndClass.php b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SkipDifferentSetAndClass.php new file mode 100644 index 00000000..ee3679b7 --- /dev/null +++ b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SkipDifferentSetAndClass.php @@ -0,0 +1,13 @@ +services(); + + $services->set(SomeClassToBeSet::class) + ->class('AnotherValue'); +}; diff --git a/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SkipSoleSet.php b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SkipSoleSet.php new file mode 100644 index 00000000..39ca0fa6 --- /dev/null +++ b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Fixture/SkipSoleSet.php @@ -0,0 +1,12 @@ +services(); + + $services->set(SomeClassToBeSet::class); +}; diff --git a/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/NoSetClassServiceDuplicationRuleTest.php b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/NoSetClassServiceDuplicationRuleTest.php new file mode 100644 index 00000000..a914c915 --- /dev/null +++ b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/NoSetClassServiceDuplicationRuleTest.php @@ -0,0 +1,41 @@ +analyse([$filePath], $expectedErrorMessagesWithLines); + } + + public static function provideData(): Iterator + { + yield [__DIR__ . '/Fixture/SetAndClassConfig.php', [ + [ + sprintf(NoSetClassServiceDuplicationRule::ERROR_MESSAGE, 'SomeClassToBeSet::class', 'SomeClassToBeSet::class', 'SomeClassToBeSet::class'), + 11, + ], + ]]; + + yield [__DIR__ . '/Fixture/SkipDifferentSetAndClass.php', []]; + yield [__DIR__ . '/Fixture/SkipSoleSet.php', []]; + } + + protected function getRule(): Rule + { + return new NoSetClassServiceDuplicationRule(); + } +} diff --git a/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Source/SomeClassToBeSet.php b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Source/SomeClassToBeSet.php new file mode 100644 index 00000000..b55fdd86 --- /dev/null +++ b/tests/Rules/Symfony/ConfigClosure/NoSetClassServiceDuplicationRule/Source/SomeClassToBeSet.php @@ -0,0 +1,8 @@ +