diff --git a/src/Helper/NamingHelper.php b/src/Helper/NamingHelper.php index 21116307..3157f080 100644 --- a/src/Helper/NamingHelper.php +++ b/src/Helper/NamingHelper.php @@ -5,6 +5,7 @@ namespace Symplify\PHPStanRules\Helper; use PhpParser\Node; +use PhpParser\Node\Expr\Variable; use PhpParser\Node\Identifier; use PhpParser\Node\Name; @@ -12,6 +13,10 @@ final class NamingHelper { public static function getName(Node $node): ?string { + if ($node instanceof Variable && is_string($node->name)) { + return $node->name; + } + if ($node instanceof Identifier || $node instanceof Name) { return $node->toString(); } @@ -21,11 +26,7 @@ public static function getName(Node $node): ?string public static function isName(Node $node, string $name): bool { - if ($node instanceof Identifier || $node instanceof Name) { - return $node->toString() === $name; - } - - return false; + return self::getName($node) === $name; } /** diff --git a/src/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule.php b/src/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule.php index fd3b9921..ea0a7b99 100644 --- a/src/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule.php +++ b/src/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule.php @@ -7,82 +7,125 @@ use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\MethodCall; +use PhpParser\NodeFinder; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use Symplify\PHPStanRules\Enum\RuleIdentifier\SymfonyRuleIdentifier; use Symplify\PHPStanRules\Helper\NamingHelper; +use Symplify\PHPStanRules\Symfony\NodeAnalyzer\SymfonyClosureDetector; /** - * @implements Rule + * @implements Rule * * @see \Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoServiceSameNameSetClassRule\NoServiceSameNameSetClassRuleTest */ -final class NoServiceSameNameSetClassRule implements Rule +final readonly class NoServiceSameNameSetClassRule implements Rule { /** * @var string */ public const ERROR_MESSAGE = 'No need to duplicate service class and name. Use only "$services->set(%s::class)" instead'; + private NodeFinder $nodeFinder; + + public function __construct() + { + $this->nodeFinder = new NodeFinder(); + } + public function getNodeType(): string { - return MethodCall::class; + return Closure::class; } /** - * @param MethodCall $node + * @param Closure $node * @return RuleError[] */ public function processNode(Node $node, Scope $scope): array { - if ($node->isFirstClassCallable()) { + if (! SymfonyClosureDetector::detect($node)) { return []; } - if (! NamingHelper::isName($node->name, 'set')) { - return []; + /** @var MethodCall[] $methodCalls */ + $methodCalls = $this->nodeFinder->findInstanceOf($node, MethodCall::class); + + $ruleErrors = []; + + foreach ($methodCalls as $methodCall) { + if ($methodCall->isFirstClassCallable()) { + continue; + } + + if (! NamingHelper::isName($methodCall->var, 'services')) { + continue; + } + + if (! NamingHelper::isName($methodCall->name, 'set')) { + continue; + } + + $serviceNameValue = $this->matchTwoArgsOfSameClassConstName($methodCall); + if (! is_string($serviceNameValue)) { + continue; + } + + if (str_contains($serviceNameValue, '\\')) { + $serviceNameValue = Strings::after($serviceNameValue, '\\', -1); + } + + $identifierRuleError = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $serviceNameValue)) + ->identifier(SymfonyRuleIdentifier::NO_SERVICE_SAME_NAME_SET_CLASS) + ->line($methodCall->getStartLine()) + ->build(); + + $ruleErrors[] = $identifierRuleError; } - if (count($node->getArgs()) !== 2) { - return []; + return $ruleErrors; + } + + /** + * We look for: + * + * $services->set(SomeClass::class, SomeClass::class) + */ + private function matchTwoArgsOfSameClassConstName(MethodCall $methodCall): ?string + { + if (count($methodCall->getArgs()) !== 2) { + return null; } - $serviceName = $node->getArgs()[0]->value; - $serviceType = $node->getArgs()[1]->value; + $serviceName = $methodCall->getArgs()[0]->value; + $serviceType = $methodCall->getArgs()[1]->value; if (! $serviceName instanceof ClassConstFetch) { - return []; + return null; } if (! $serviceType instanceof ClassConstFetch) { - return []; + return null; } $serviceNameValue = NamingHelper::getName($serviceName->class); if (! is_string($serviceNameValue)) { - return []; + return null; } $serviceTypeValue = NamingHelper::getName($serviceType->class); if (! is_string($serviceTypeValue)) { - return []; + return null; } if ($serviceNameValue !== $serviceTypeValue) { - return []; + return null; } - if (str_contains($serviceNameValue, '\\')) { - $serviceNameValue = Strings::after($serviceNameValue, '\\', -1); - } - - $identifierRuleError = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $serviceNameValue)) - ->identifier(SymfonyRuleIdentifier::NO_SERVICE_SAME_NAME_SET_CLASS) - ->build(); - - return [$identifierRuleError]; + return $serviceNameValue; } } diff --git a/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SkipNonClosureConstantSet.php b/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SkipNonClosureConstantSet.php new file mode 100644 index 00000000..c44a02f1 --- /dev/null +++ b/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SkipNonClosureConstantSet.php @@ -0,0 +1,18 @@ +set(self::NAME, self::TYPE); + } +} diff --git a/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SkipParametersSetConstant.php b/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SkipParametersSetConstant.php new file mode 100644 index 00000000..92d4aef6 --- /dev/null +++ b/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SkipParametersSetConstant.php @@ -0,0 +1,12 @@ +parameters(); + $parameters->set(ConstantList::NAME, ConstantList::NAME); +}; diff --git a/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SomeConfigWithInvalidSet.php b/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SomeConfigWithInvalidSet.php index 74241287..27ba06ea 100644 --- a/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SomeConfigWithInvalidSet.php +++ b/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Fixture/SomeConfigWithInvalidSet.php @@ -1,6 +1,6 @@ getByType(NoServiceSameNameSetClassRule::class); } } diff --git a/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Source/ConstantList.php b/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Source/ConstantList.php new file mode 100644 index 00000000..163e5c91 --- /dev/null +++ b/tests/Rules/Symfony/ConfigClosure/NoServiceSameNameSetClassRule/Source/ConstantList.php @@ -0,0 +1,8 @@ +