From f9e7cb2907527aa0da85e7039105444ce13e674c Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 31 Mar 2026 10:25:35 +0200 Subject: [PATCH] Use external file dependencies instead of ResultCacheMetaExtension Replace SymfonyContainerResultCacheMetaExtension (which caused full cache invalidation on any container change) with PHPStan's new external file dependency tracking. Now when the container XML changes, only files that actually call Container::get(), Container::has(), getParameter(), or hasParameter() are re-analyzed instead of the entire project. All extensions and rules that read from ServiceMap or ParameterMap now register the container XML path as an external file dependency via ExternalFileDependencyRegistrar. The new parameters are optional (nullable) for backward compatibility with older PHPStan versions. Co-Authored-By: Claude Code --- extension.neon | 23 +++++++++---------- rules.neon | 14 +++++++++-- .../ContainerInterfacePrivateServiceRule.php | 16 ++++++++++++- .../ContainerInterfaceUnknownServiceRule.php | 17 +++++++++++++- .../ParameterDynamicReturnTypeExtension.php | 22 +++++++++++++++++- .../ServiceDynamicReturnTypeExtension.php | 20 +++++++++++++++- 6 files changed, 94 insertions(+), 18 deletions(-) diff --git a/extension.neon b/extension.neon index f8516ed0..809f5974 100644 --- a/extension.neon +++ b/extension.neon @@ -137,16 +137,16 @@ services: # ControllerTrait::get()/has() return type - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, %symfony.constantHassers%) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, %symfony.constantHassers%, containerXmlPath: %symfony.containerXmlPath%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Psr\Container\ContainerInterface, %symfony.constantHassers%) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Psr\Container\ContainerInterface, %symfony.constantHassers%, containerXmlPath: %symfony.containerXmlPath%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, %symfony.constantHassers%) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, %symfony.constantHassers%, containerXmlPath: %symfony.containerXmlPath%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, %symfony.constantHassers%) + factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, %symfony.constantHassers%, containerXmlPath: %symfony.containerXmlPath%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # ControllerTrait::has() type specification @@ -314,20 +314,20 @@ services: # ParameterBagInterface::get()/has() return type - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface, 'get', 'has', %symfony.constantHassers%) + factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface, 'get', 'has', %symfony.constantHassers%, containerXmlPath: %symfony.containerXmlPath%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # ContainerInterface::getParameter()/hasParameter() return type - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, 'getParameter', 'hasParameter', %symfony.constantHassers%) + factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface, 'getParameter', 'hasParameter', %symfony.constantHassers%, containerXmlPath: %symfony.containerXmlPath%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # (Abstract)Controller::getParameter() return type - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, 'getParameter', null, %symfony.constantHassers%) + factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\AbstractController, 'getParameter', null, %symfony.constantHassers%, containerXmlPath: %symfony.containerXmlPath%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, 'getParameter', null, %symfony.constantHassers%) + factory: PHPStan\Type\Symfony\ParameterDynamicReturnTypeExtension(Symfony\Bundle\FrameworkBundle\Controller\Controller, 'getParameter', null, %symfony.constantHassers%, containerXmlPath: %symfony.containerXmlPath%) tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - class: PHPStan\Symfony\InputBagStubFilesExtension @@ -375,7 +375,6 @@ services: factory: PHPStan\Type\Symfony\ExtensionGetConfigurationReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] - - - class: PHPStan\Symfony\SymfonyContainerResultCacheMetaExtension - tags: - - phpstan.resultCacheMetaExtension + # SymfonyContainerResultCacheMetaExtension removed: replaced by external file + # dependency tracking which provides incremental (per-file) cache invalidation + # instead of full cache invalidation when the container XML changes. diff --git a/rules.neon b/rules.neon index cedcea7a..beabc71b 100644 --- a/rules.neon +++ b/rules.neon @@ -1,8 +1,18 @@ rules: - - PHPStan\Rules\Symfony\ContainerInterfacePrivateServiceRule - - PHPStan\Rules\Symfony\ContainerInterfaceUnknownServiceRule - PHPStan\Rules\Symfony\UndefinedArgumentRule - PHPStan\Rules\Symfony\InvalidArgumentDefaultValueRule - PHPStan\Rules\Symfony\UndefinedOptionRule - PHPStan\Rules\Symfony\InvalidOptionDefaultValueRule +services: + - + class: PHPStan\Rules\Symfony\ContainerInterfacePrivateServiceRule + arguments: + containerXmlPath: %symfony.containerXmlPath% + tags: [phpstan.rules.rule] + - + class: PHPStan\Rules\Symfony\ContainerInterfaceUnknownServiceRule + arguments: + containerXmlPath: %symfony.containerXmlPath% + tags: [phpstan.rules.rule] + diff --git a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php index c8ec11a8..667e1082 100644 --- a/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfacePrivateServiceRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; +use PHPStan\Analyser\ExternalFileDependencyRegistrar; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -21,9 +22,19 @@ final class ContainerInterfacePrivateServiceRule implements Rule private ServiceMap $serviceMap; - public function __construct(ServiceMap $symfonyServiceMap) + private ?string $containerXmlPath; + + private ?ExternalFileDependencyRegistrar $externalFileDependencyRegistrar; + + public function __construct( + ServiceMap $symfonyServiceMap, + ?string $containerXmlPath = null, + ?ExternalFileDependencyRegistrar $externalFileDependencyRegistrar = null, + ) { $this->serviceMap = $symfonyServiceMap; + $this->containerXmlPath = $containerXmlPath; + $this->externalFileDependencyRegistrar = $externalFileDependencyRegistrar; } public function getNodeType(): string @@ -66,6 +77,9 @@ public function processNode(Node $node, Scope $scope): array $serviceId = $this->serviceMap::getServiceIdFromNode($node->getArgs()[0]->value, $scope); if ($serviceId !== null) { + if ($this->containerXmlPath !== null && $this->externalFileDependencyRegistrar !== null) { + $this->externalFileDependencyRegistrar->add($this->containerXmlPath); + } $service = $this->serviceMap->getService($serviceId); if ($service !== null && !$service->isPublic()) { return [ diff --git a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php index 23444b6b..f5399ab6 100644 --- a/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php +++ b/src/Rules/Symfony/ContainerInterfaceUnknownServiceRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; +use PHPStan\Analyser\ExternalFileDependencyRegistrar; use PHPStan\Analyser\Scope; use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; @@ -23,10 +24,21 @@ final class ContainerInterfaceUnknownServiceRule implements Rule private Printer $printer; - public function __construct(ServiceMap $symfonyServiceMap, Printer $printer) + private ?string $containerXmlPath; + + private ?ExternalFileDependencyRegistrar $externalFileDependencyRegistrar; + + public function __construct( + ServiceMap $symfonyServiceMap, + Printer $printer, + ?string $containerXmlPath = null, + ?ExternalFileDependencyRegistrar $externalFileDependencyRegistrar = null, + ) { $this->serviceMap = $symfonyServiceMap; $this->printer = $printer; + $this->containerXmlPath = $containerXmlPath; + $this->externalFileDependencyRegistrar = $externalFileDependencyRegistrar; } public function getNodeType(): string @@ -65,6 +77,9 @@ public function processNode(Node $node, Scope $scope): array $serviceId = $this->serviceMap::getServiceIdFromNode($node->getArgs()[0]->value, $scope); if ($serviceId !== null) { + if ($this->containerXmlPath !== null && $this->externalFileDependencyRegistrar !== null) { + $this->externalFileDependencyRegistrar->add($this->containerXmlPath); + } $service = $this->serviceMap->getService($serviceId); $serviceIdType = $scope->getType($node->getArgs()[0]->value); if ($service === null && !$scope->getType(Helper::createMarkerNode($node->var, $serviceIdType, $this->printer))->equals($serviceIdType)) { diff --git a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php index 687b0c33..19fa0814 100644 --- a/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ParameterDynamicReturnTypeExtension.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Symfony; use PhpParser\Node\Expr\MethodCall; +use PHPStan\Analyser\ExternalFileDependencyRegistrar; use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\MethodReflection; @@ -54,6 +55,10 @@ final class ParameterDynamicReturnTypeExtension implements DynamicMethodReturnTy private TypeStringResolver $typeStringResolver; + private ?string $containerXmlPath; + + private ?ExternalFileDependencyRegistrar $externalFileDependencyRegistrar; + /** * @param class-string $className */ @@ -63,7 +68,9 @@ public function __construct( ?string $methodHas, bool $constantHassers, ParameterMap $symfonyParameterMap, - TypeStringResolver $typeStringResolver + TypeStringResolver $typeStringResolver, + ?string $containerXmlPath = null, + ?ExternalFileDependencyRegistrar $externalFileDependencyRegistrar = null, ) { $this->className = $className; @@ -72,6 +79,8 @@ public function __construct( $this->constantHassers = $constantHassers; $this->parameterMap = $symfonyParameterMap; $this->typeStringResolver = $typeStringResolver; + $this->containerXmlPath = $containerXmlPath; + $this->externalFileDependencyRegistrar = $externalFileDependencyRegistrar; } public function getClass(): string @@ -122,6 +131,8 @@ private function getGetTypeFromMethodCall( return $defaultReturnType; } + $this->registerExternalDependency(); + $returnTypes = []; foreach ($parameterKeys as $parameterKey) { $parameter = $this->parameterMap->getParameter($parameterKey); @@ -203,6 +214,13 @@ private function generalizeType(Type $type): Type }); } + private function registerExternalDependency(): void + { + if ($this->containerXmlPath !== null && $this->externalFileDependencyRegistrar !== null) { + $this->externalFileDependencyRegistrar->add($this->containerXmlPath); + } + } + private function getHasTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, @@ -218,6 +236,8 @@ private function getHasTypeFromMethodCall( return null; } + $this->registerExternalDependency(); + $has = null; foreach ($parameterKeys as $parameterKey) { $parameter = $this->parameterMap->getParameter($parameterKey); diff --git a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php index 0667d30c..deae075f 100644 --- a/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ServiceDynamicReturnTypeExtension.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Symfony; use PhpParser\Node\Expr\MethodCall; +use PHPStan\Analyser\ExternalFileDependencyRegistrar; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\ShouldNotHappenException; @@ -30,6 +31,10 @@ final class ServiceDynamicReturnTypeExtension implements DynamicMethodReturnType private ParameterMap $parameterMap; + private ?string $containerXmlPath; + + private ?ExternalFileDependencyRegistrar $externalFileDependencyRegistrar; + private ?ParameterBag $parameterBag = null; /** @@ -39,13 +44,17 @@ public function __construct( string $className, bool $constantHassers, ServiceMap $symfonyServiceMap, - ParameterMap $symfonyParameterMap + ParameterMap $symfonyParameterMap, + ?string $containerXmlPath = null, + ?ExternalFileDependencyRegistrar $externalFileDependencyRegistrar = null, ) { $this->className = $className; $this->constantHassers = $constantHassers; $this->serviceMap = $symfonyServiceMap; $this->parameterMap = $symfonyParameterMap; + $this->containerXmlPath = $containerXmlPath; + $this->externalFileDependencyRegistrar = $externalFileDependencyRegistrar; } public function getClass(): string @@ -86,6 +95,7 @@ private function getGetTypeFromMethodCall( $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); if ($serviceId !== null) { + $this->registerExternalDependency(); $service = $this->serviceMap->getService($serviceId); if ($service !== null && (!$service->isSynthetic() || $service->getClass() !== null)) { return new ObjectType($this->determineServiceClass($parameterBag, $service) ?? $serviceId); @@ -131,6 +141,7 @@ private function getHasTypeFromMethodCall( $serviceId = $this->serviceMap::getServiceIdFromNode($methodCall->getArgs()[0]->value, $scope); if ($serviceId !== null) { + $this->registerExternalDependency(); $service = $this->serviceMap->getService($serviceId); return new ConstantBooleanType($service !== null && $service->isPublic()); } @@ -138,6 +149,13 @@ private function getHasTypeFromMethodCall( return null; } + private function registerExternalDependency(): void + { + if ($this->containerXmlPath !== null && $this->externalFileDependencyRegistrar !== null) { + $this->externalFileDependencyRegistrar->add($this->containerXmlPath); + } + } + private function determineServiceClass(ParameterBag $parameterBag, ServiceDefinition $service): ?string { $class = $service->getClass();