From b0fee692cfe8735477bde32f3773c130bd5f7454 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 19 Oct 2023 16:06:06 +0200 Subject: [PATCH] Resolve services subscribed to with ServiceSubscriberTrait --- src/Symfony/DefaultServiceMap.php | 116 ++++++++++++++++++ ...ntainerInterfaceUnknownServiceRuleTest.php | 35 ++++++ .../Symfony/ExampleServiceTraitSubscriber.php | 70 +++++++++++ tests/Rules/Symfony/container.xml | 1 + 4 files changed, 222 insertions(+) create mode 100644 tests/Rules/Symfony/ExampleServiceTraitSubscriber.php diff --git a/src/Symfony/DefaultServiceMap.php b/src/Symfony/DefaultServiceMap.php index cb8259d2..6d6ed3a3 100644 --- a/src/Symfony/DefaultServiceMap.php +++ b/src/Symfony/DefaultServiceMap.php @@ -2,13 +2,19 @@ namespace PHPStan\Symfony; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\VariadicPlaceholder; +use PhpParser\Node\Scalar\MagicConst\Method; use PHPStan\Analyser\Scope; +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionNamedType; use PHPStan\Type\TypeUtils; use function count; final class DefaultServiceMap implements ServiceMap { + const AUTOWIRE_ATTRIBUTE_CLASS = 'Symfony\Component\DependencyInjection\Attribute\Autowire'; /** @var ServiceDefinition[] */ private $services; @@ -36,8 +42,118 @@ public function getService(string $id): ?ServiceDefinition public static function getServiceIdFromNode(Expr $node, Scope $scope): ?string { + if ($node instanceof Method) { + $serviceId = self::getServiceSubscriberId($node, $scope); + if (null !== $serviceId) { + return $serviceId; + } + } + $strings = TypeUtils::getConstantStrings($scope->getType($node)); return count($strings) === 1 ? $strings[0]->getValue() : null; } + private static function getServiceSubscriberId(Expr $node, Scope $scope): ?string + { + $classReflection = $scope->getClassReflection(); + $functionName = $scope->getFunctionName(); + if ($classReflection === null || + $functionName === null || + !$classReflection->hasTraitUse('Symfony\Contracts\Service\ServiceSubscriberTrait') + ) { + return null; + } + + $methodReflection = $classReflection->getNativeReflection(); + $method = $methodReflection->getMethod($functionName); + $attributes = $method->getAttributes('Symfony\Contracts\Service\Attribute\SubscribedService'); + if (count($attributes) !== 1) { + return null; + } + + $arguments = $attributes[0]->getArgumentsExpressions(); + + $autowireArgs = isset($arguments['attributes']) ? + self::getAutowireArguments($arguments['attributes'], $scope) : + []; + + $value = null; + $ignoreValue = false; + foreach ($autowireArgs as $key => $arg) { + if (!$arg instanceof Arg) { + continue; + } + + if ($arg->name !== null) { + $argName = $arg->name->toString(); + } elseif ($key === 0) { + $argName = 'value'; + } elseif ($key === 1) { + $argName = 'service'; + } else { + continue; + } + + // `service` argument overrules others, so break loop + $argValue = TypeUtils::getConstantStrings($scope->getType($arg->value)); + $argValue = count($argValue) === 1 ? $argValue[0]->getValue() : null; + + if (!is_string($argValue)) { + continue; + } + + if ($argName === 'service') { + return $argValue; + } + + if ($argName === 'value') { + $value = str_starts_with($argValue, '@') && + !str_starts_with($argValue, '@@') && + !str_starts_with($argValue, '@=') ? + substr($argValue, 1) : + $argValue; + } else { + // Can't `break` because `service` has higher priority than others + $ignoreValue = true; + } + } + + // If value is provided, and no other arguments with higher priority the value is the service id + if (!$ignoreValue && $value !== null) { + return $value; + } + + $returnType = $method->getReturnType(); + return $returnType instanceof ReflectionNamedType ? $returnType->getName() : null; + } + + /** @return array */ + private static function getAutowireArguments(Expr $expression, Scope $scope): array + { + if ($expression instanceof Expr\New_) { + return $expression->class instanceof Name && + $scope->resolveName($expression->class) === self::AUTOWIRE_ATTRIBUTE_CLASS ? + $expression->args : + []; + } + + if ($expression instanceof Expr\Array_) { + foreach ($expression->items as $item) { + if ($item === null) { + continue; + } + + $arg = $item->value; + if ($arg instanceof Expr\New_) { + return $arg->class instanceof Name && + $scope->resolveName($arg->class) === self::AUTOWIRE_ATTRIBUTE_CLASS ? + $arg->args : + []; + } + } + } + + return []; + } + } diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php index 4bd233e9..e0414641 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php @@ -72,6 +72,41 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void ); } + public function testGetPrivateServiceInServiceSubscriberWithTrait(): void + { + if (!interface_exists('Symfony\Contracts\Service\ServiceSubscriberInterface')) { + self::markTestSkipped('The test needs Symfony\Contracts\Service\ServiceSubscriberInterface class.'); + } + + if (!trait_exists('Symfony\Contracts\Service\ServiceSubscriberTrait')) { + self::markTestSkipped('The test needs Symfony\Contracts\Service\ServiceSubscriberTrait class.'); + } + + $this->analyse( + [ + __DIR__ . '/ExampleServiceTraitSubscriber.php', + ], + [ + [ + 'Service "unknown" is not registered in the container.', + 44, + ], + [ + 'Service "unknown" is not registered in the container.', + 50, + ], + [ + 'Service "unknown" is not registered in the container.', + 56, + ], + [ + 'Service "Baz" is not registered in the container.', + 68, + ], + ] + ); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/Rules/Symfony/ExampleServiceTraitSubscriber.php b/tests/Rules/Symfony/ExampleServiceTraitSubscriber.php new file mode 100644 index 00000000..681c306b --- /dev/null +++ b/tests/Rules/Symfony/ExampleServiceTraitSubscriber.php @@ -0,0 +1,70 @@ +locator = $locator; + } + + #[SubscribedService(attributes: new Autowire('private'))] + private function getIndexed(): Foo + { + return $this->locator->get(__METHOD__); + } + + #[SubscribedService(attributes: new Autowire(value: 'private'))] + private function getNamedValue(): Foo + { + return $this->locator->get(__METHOD__); + } + + #[SubscribedService(attributes: new Autowire(service: 'private'))] + private function getNamedService(): Foo + { + return $this->locator->get(__METHOD__); + } + + #[SubscribedService(attributes: new Autowire('unknown'))] + private function getUnknownIndexed(): Foo + { + return $this->locator->get(__METHOD__); + } + + #[SubscribedService(attributes: new Autowire(value: 'unknown'))] + private function getUnknownNamedValue(): Foo + { + return $this->locator->get(__METHOD__); + } + + #[SubscribedService(attributes: new Autowire(service: 'unknown'))] + private function getUnknownNamedService(): Foo + { + return $this->locator->get(__METHOD__); + } + + #[SubscribedService] + public function getBar(): \Bar + { + return $this->locator->get(__METHOD__); + } + + #[SubscribedService] + public function getBaz(): \Baz + { + return $this->locator->get(__METHOD__); + } +} diff --git a/tests/Rules/Symfony/container.xml b/tests/Rules/Symfony/container.xml index f3261e0a..85029410 100644 --- a/tests/Rules/Symfony/container.xml +++ b/tests/Rules/Symfony/container.xml @@ -8,5 +8,6 @@ +