From 9bb1d4358d0b3c074006a209bd73d3343a610c60 Mon Sep 17 00:00:00 2001 From: Matt Glaman Date: Fri, 17 Jul 2026 09:04:22 -0500 Subject: [PATCH] Fix never type for dynamic container service IDs (#1014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(type): avoid never type for dynamic container service IDs Container get()/has() built their return type by unioning the types of each constant string argument. A variable service ID has none, so the union of zero types collapsed to never — a subtype of everything — triggering entityStoragePropertyAssignment on core-generated proxy classes. Fall back to the declared return type instead. Closes #1012 Co-authored-by: Claude Fable 5 --- .../ContainerDynamicReturnTypeExtension.php | 16 ++++++++- ...ntityStoragePropertyAssignmentRuleTest.php | 5 +++ tests/src/Rules/data/bug-1012-proxy.php | 33 +++++++++++++++++++ .../DrupalContainerDynamicReturnTypeTest.php | 1 + tests/src/Type/data/bug-1012.php | 15 +++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/src/Rules/data/bug-1012-proxy.php create mode 100644 tests/src/Type/data/bug-1012.php diff --git a/src/Type/ContainerDynamicReturnTypeExtension.php b/src/Type/ContainerDynamicReturnTypeExtension.php index 3a071ed7..ad3aa26d 100644 --- a/src/Type/ContainerDynamicReturnTypeExtension.php +++ b/src/Type/ContainerDynamicReturnTypeExtension.php @@ -67,6 +67,12 @@ public function getTypeFromMethodCall( } } + // A dynamic service ID has no constant strings; unioning zero + // types would produce `never`. + if ($types === []) { + return $returnType; + } + return TypeCombinator::union(...$types); } elseif ($methodName === 'get') { $args = $methodCall->getArgs(); @@ -89,12 +95,20 @@ public function getTypeFromMethodCall( $argType = $scope->getType($args[0]->value); - foreach ($argType->getConstantStrings() as $constantStringType) { + $constantStrings = $argType->getConstantStrings(); + foreach ($constantStrings as $constantStringType) { $serviceId = $constantStringType->getValue(); $service = $this->serviceMap->getService($serviceId); $types[] = $service !== null ? $service->getType() : $returnType; } + // A dynamic service ID has no constant strings; fall back to the + // declared return type so the union cannot collapse to `never`, + // while keeping any null added for NULL_ON_INVALID_REFERENCE. + if ($constantStrings === []) { + $types[] = $returnType; + } + return TypeCombinator::union(...$types); } diff --git a/tests/src/Rules/EntityStoragePropertyAssignmentRuleTest.php b/tests/src/Rules/EntityStoragePropertyAssignmentRuleTest.php index 451afb5f..aba4ea6f 100644 --- a/tests/src/Rules/EntityStoragePropertyAssignmentRuleTest.php +++ b/tests/src/Rules/EntityStoragePropertyAssignmentRuleTest.php @@ -44,4 +44,9 @@ public function testRule(): void ] ); } + + public function testBug1012(): void + { + $this->analyse([__DIR__ . '/data/bug-1012-proxy.php'], []); + } } diff --git a/tests/src/Rules/data/bug-1012-proxy.php b/tests/src/Rules/data/bug-1012-proxy.php new file mode 100644 index 00000000..e30634ef --- /dev/null +++ b/tests/src/Rules/data/bug-1012-proxy.php @@ -0,0 +1,33 @@ +container = $container; + $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id; + } + + protected function lazyLoadItself(): object + { + if (!isset($this->service)) { + $this->service = $this->container->get($this->drupalProxyOriginalServiceId); + } + + return $this->service; + } +} diff --git a/tests/src/Type/DrupalContainerDynamicReturnTypeTest.php b/tests/src/Type/DrupalContainerDynamicReturnTypeTest.php index 10e29aa6..73db87ce 100644 --- a/tests/src/Type/DrupalContainerDynamicReturnTypeTest.php +++ b/tests/src/Type/DrupalContainerDynamicReturnTypeTest.php @@ -17,6 +17,7 @@ public static function dataFileAsserts(): iterable yield from self::gatherAssertTypes(__DIR__ . '/data/drupal-service-static.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/drupal-class-resolver.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/bug-563.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-1012.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/container-optional.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/synthetic.php'); } diff --git a/tests/src/Type/data/bug-1012.php b/tests/src/Type/data/bug-1012.php new file mode 100644 index 00000000..474252b5 --- /dev/null +++ b/tests/src/Type/data/bug-1012.php @@ -0,0 +1,15 @@ +get($service_id)); + assertType('object|null', $container->get($service_id, ContainerInterface::NULL_ON_INVALID_REFERENCE)); + assertType('bool', $container->has($service_id)); +}