diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php new file mode 100644 index 0000000000000..ed852fd041942 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Find all service tags which are defined, but not used and yield a warning log message. + * + * @author Florian Pfitzer + */ +class UnusedTagsPass implements CompilerPassInterface +{ + private $whitelist = array( + 'console.command', + 'config_cache.resource_checker', + 'data_collector', + 'form.type', + 'form.type_extension', + 'form.type_guesser', + 'kernel.cache_clearer', + 'kernel.cache_warmer', + 'kernel.event_listener', + 'kernel.event_subscriber', + 'kernel.fragment_renderer', + 'monolog.logger', + 'routing.expression_language_provider', + 'routing.loader', + 'security.expression_language_provider', + 'security.remember_me_aware', + 'security.voter', + 'serializer.encoder', + 'serializer.normalizer', + 'templating.helper', + 'translation.dumper', + 'translation.extractor', + 'translation.loader', + 'twig.extension', + 'twig.loader', + 'validator.constraint_validator', + 'validator.initializer', + ); + + public function process(ContainerBuilder $container) + { + $compiler = $container->getCompiler(); + $formatter = $compiler->getLoggingFormatter(); + $tags = array_unique(array_merge($container->findTags(), $this->whitelist)); + + foreach ($container->findUnusedTags() as $tag) { + // skip whitelisted tags + if (in_array($tag, $this->whitelist)) { + continue; + } + + // check for typos + $candidates = array(); + foreach ($tags as $definedTag) { + if ($definedTag === $tag) { + continue; + } + + if (false !== strpos($definedTag, $tag) || levenshtein($tag, $definedTag) <= strlen($tag) / 3) { + $candidates[] = $definedTag; + } + } + + $services = array_keys($container->findTaggedServiceIds($tag)); + $message = sprintf('Tag "%s" was defined on service(s) "%s", but was never used.', $tag, implode('", "', $services)); + if (!empty($candidates)) { + $message .= sprintf(' Did you mean "%s"?', implode('", "', $candidates)); + } + + $compiler->addLogMessage($formatter->format($this, $message)); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 7b1c77f225e57..3a96f71e1176d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -28,6 +28,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumperPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigCachePass; use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -91,6 +92,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new SerializerPass()); if ($container->getParameter('kernel.debug')) { + $container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new CompilerDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new ConfigCachePass()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php new file mode 100644 index 0000000000000..f354007bb982d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass; + +class UnusedTagsPassTest extends \PHPUnit_Framework_TestCase +{ + public function testProcess() + { + $pass = new UnusedTagsPass(); + + $formatter = $this->getMock('Symfony\Component\DependencyInjection\Compiler\LoggingFormatter'); + $formatter + ->expects($this->at(0)) + ->method('format') + ->with($pass, 'Tag "kenrel.event_subscriber" was defined on service(s) "foo", "bar", but was never used. Did you mean "kernel.event_subscriber"?') + ; + + $compiler = $this->getMock('Symfony\Component\DependencyInjection\Compiler\Compiler'); + $compiler->expects($this->once())->method('getLoggingFormatter')->will($this->returnValue($formatter)); + + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder', + array('findTaggedServiceIds', 'getCompiler', 'findUnusedTags', 'findTags') + ); + $container->expects($this->once())->method('getCompiler')->will($this->returnValue($compiler)); + $container->expects($this->once()) + ->method('findTags') + ->will($this->returnValue(array('kenrel.event_subscriber'))); + $container->expects($this->once()) + ->method('findUnusedTags') + ->will($this->returnValue(array('kenrel.event_subscriber', 'form.type'))); + $container->expects($this->once()) + ->method('findTaggedServiceIds') + ->with('kenrel.event_subscriber') + ->will($this->returnValue(array( + 'foo' => array(), + 'bar' => array(), + ))); + + $pass->process($container); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php b/src/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php index 6bd6161ceb441..db208fa0d63c1 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/LoggingFormatter.php @@ -20,7 +20,7 @@ class LoggingFormatter { public function formatRemoveService(CompilerPassInterface $pass, $id, $reason) { - return $this->format($pass, sprintf('Removed service "%s"; reason: %s', $id, $reason)); + return $this->format($pass, sprintf('Removed service "%s"; reason: %s.', $id, $reason)); } public function formatInlineService(CompilerPassInterface $pass, $id, $target) diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 52405413af3d7..e3a6ea744c233 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -90,6 +90,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface */ private $expressionLanguageProviders = array(); + /** + * @var string[] with tag names used by findTaggedServiceIds + */ + private $usedTags = array(); + /** * Sets the track resources flag. * @@ -1064,6 +1069,7 @@ public function resolveServices($value) */ public function findTaggedServiceIds($name) { + $this->usedTags[] = $name; $tags = array(); foreach ($this->getDefinitions() as $id => $definition) { if ($definition->hasTag($name)) { @@ -1089,6 +1095,16 @@ public function findTags() return array_unique($tags); } + /** + * Returns all tags not queried by findTaggedServiceIds. + * + * @return string[] An array of tags + */ + public function findUnusedTags() + { + return array_values(array_diff($this->findTags(), $this->usedTags)); + } + public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) { $this->expressionLanguageProviders[] = $provider; diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 511b29c939ab7..047a7cf3347f4 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -560,6 +560,18 @@ public function testfindTaggedServiceIds() $this->assertEquals(array(), $builder->findTaggedServiceIds('foobar'), '->findTaggedServiceIds() returns an empty array if there is annotated services'); } + public function testFindUnusedTags() + { + $builder = new ContainerBuilder(); + $builder + ->register('foo', 'Bar\FooClass') + ->addTag('kernel.event_listener', array('foo' => 'foo')) + ->addTag('kenrel.event_listener', array('bar' => 'bar')) + ; + $builder->findTaggedServiceIds('kernel.event_listener'); + $this->assertEquals(array('kenrel.event_listener'), $builder->findUnusedTags(), '->findUnusedTags() returns an array with unused tags'); + } + /** * @covers Symfony\Component\DependencyInjection\ContainerBuilder::findDefinition */