diff --git a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php index 23444b6b..b3ac409c 100644 --- a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php @@ -50,6 +50,14 @@ public function processNode(Node $node, Scope $scope): array return []; } + // A ServiceLocator is a scoped locator: its keys are local (e.g. tag index + // attributes from #[AutowireLocator]), not global container service ids, so the + // ServiceMap has no knowledge of them and cannot tell whether they are registered. + $isServiceLocatorType = (new ObjectType('Symfony\Component\DependencyInjection\ServiceLocator'))->isSuperTypeOf($argType); + if ($isServiceLocatorType->yes()) { + return []; + } + $isControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\Controller'))->isSuperTypeOf($argType); $isAbstractControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\AbstractController'))->isSuperTypeOf($argType); $isContainerType = (new ObjectType('Symfony\Component\DependencyInjection\ContainerInterface'))->isSuperTypeOf($argType); diff --git a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php index 0667d30c..b8ef4541 100644 --- a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php @@ -129,6 +129,14 @@ private function getHasTypeFromMethodCall( return null; } + // A ServiceLocator is a scoped locator: its keys are local (e.g. tag index + // attributes from #[AutowireLocator]), not global container service ids. The + // ServiceMap only knows the global container, so it cannot decide has() here; + // returning a constant bool would wrongly make guard branches unreachable. + if ((new ObjectType('Symfony\Component\DependencyInjection\ServiceLocator'))->isSuperTypeOf($scope->getType($methodCall->var))->yes()) { + return null; + } + $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); if ($serviceId !== null) { $service = $this->serviceMap->getService($serviceId); diff --git a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php index c975750f..0ed9c002 100644 --- a/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php +++ b/tests/Rules/Symfony/ContainerInterfaceUnknownServiceRuleTest.php @@ -71,6 +71,20 @@ public function testGetPrivateServiceInLegacyServiceSubscriber(): void ); } + public function testServiceLocatorKeyIsNotReportedAsUnknownService(): void + { + if (!class_exists('Symfony\Component\DependencyInjection\ServiceLocator')) { + self::markTestSkipped('The test needs Symfony\Component\DependencyInjection\ServiceLocator class.'); + } + + $this->analyse( + [ + __DIR__ . '/ExampleServiceLocatorConsumer.php', + ], + [], + ); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/Rules/Symfony/ExampleServiceLocatorConsumer.php b/tests/Rules/Symfony/ExampleServiceLocatorConsumer.php new file mode 100644 index 00000000..416a7051 --- /dev/null +++ b/tests/Rules/Symfony/ExampleServiceLocatorConsumer.php @@ -0,0 +1,24 @@ + $locator + */ + public function __construct(private readonly ServiceLocator $locator) + { + } + + public function run(): void + { + // "unknown" is not a global container service id, but it is a valid locator + // index key (e.g. from #[AutowireLocator]), so no serviceNotFound must be reported. + $this->locator->get('unknown'); + } + +} diff --git a/tests/Type/Symfony/ExtensionTest.php b/tests/Type/Symfony/ExtensionTest.php index 7e965cf5..7a2f37f8 100644 --- a/tests/Type/Symfony/ExtensionTest.php +++ b/tests/Type/Symfony/ExtensionTest.php @@ -33,6 +33,8 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/tree_builder.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/service_locator.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleBaseCommand.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleOptionCommand.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/ExampleOptionLazyCommand.php'); diff --git a/tests/Type/Symfony/data/service_locator.php b/tests/Type/Symfony/data/service_locator.php new file mode 100644 index 00000000..38c1a3d3 --- /dev/null +++ b/tests/Type/Symfony/data/service_locator.php @@ -0,0 +1,37 @@ + $locator + */ + public function __construct( + private readonly ServiceLocator $locator, + ) + { + } + + public function run(string $key): void + { + // has() on a ServiceLocator must NOT collapse to a constant bool: a locator key + // is a local index, not a global service id, so the ServiceMap cannot decide it. + // Otherwise a `if (!$locator->has('x')) { return; }` guard would be seen as + // "always true" and everything after it as unreachable. + assertType('bool', $this->locator->has($key)); + assertType('bool', $this->locator->has('some_index')); + } + +}