Make it really work on real apps

This commit is contained in:
Nicolas Grekas 2019-10-12 14:06:59 +02:00
parent 4b3e9d4c96
commit 8230a1543e
22 changed files with 138 additions and 47 deletions

View File

@ -4,7 +4,7 @@ CHANGELOG
4.4.0
-----
* Added `lint:container` to check that services wiring matches type declarations
* Added `lint:container` command to check that services wiring matches type declarations
* Added `MailerAssertionsTrait`
* Deprecated support for `templating` engine in `TemplateController`, use Twig instead
* Deprecated the `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()`

View File

@ -15,7 +15,6 @@ use Symfony\Component\Config\ConfigCache;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@ -39,7 +38,6 @@ final class ContainerLintCommand extends Command
$this
->setDescription('Ensures that arguments injected into services match type declarations')
->setHelp('This command parses service definitions and ensures that injected values match the type declarations of each services\' class.')
->addOption('ignore-unused-services', 'o', InputOption::VALUE_NONE, 'Ignore unused services')
;
}
@ -50,13 +48,11 @@ final class ContainerLintCommand extends Command
{
$container = $this->getContainerBuilder();
$container->setParameter('container.build_hash', 'lint_container');
$container->setParameter('container.build_time', time());
$container->setParameter('container.build_id', 'lint_container');
$container->addCompilerPass(
new CheckTypeDeclarationsPass(true),
$input->getOption('ignore-unused-services') ? PassConfig::TYPE_AFTER_REMOVING : PassConfig::TYPE_OPTIMIZE,
-5
);
$container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
$container->compile();

View File

@ -17,7 +17,7 @@
</service>
<service id="Symfony\Component\Validator\Validator\ValidatorInterface" alias="validator" />
<service id="validator.builder" class="Symfony\Component\Validator\ValidatorBuilderInterface">
<service id="validator.builder" class="Symfony\Component\Validator\ValidatorBuilder">
<factory class="Symfony\Component\Validator\Validation" method="createValidatorBuilder" />
<call method="setConstraintValidatorFactory">
<argument type="service" id="validator.validator_factory" />

View File

@ -14,6 +14,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\AnnotationReaderPass;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\Config\CustomConfig;
use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\TranslationDebugPass;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@ -31,5 +33,15 @@ class TestBundle extends Bundle
$container->addCompilerPass(new AnnotationReaderPass(), PassConfig::TYPE_AFTER_REMOVING);
$container->addCompilerPass(new TranslationDebugPass());
$container->addCompilerPass(new class() implements CompilerPassInterface {
public function process(ContainerBuilder $container)
{
$container->removeDefinition('twig.controller.exception');
$container->removeDefinition('twig.controller.preview_error');
}
});
$container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle;
use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class TestBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->setParameter('container.build_hash', 'test_bundle');
$container->setParameter('container.build_time', time());
$container->setParameter('container.build_id', 'test_bundle');
$container->addCompilerPass(new class() implements CompilerPassInterface {
public function process(ContainerBuilder $container)
{
$container->removeDefinition('twig.controller.exception');
$container->removeDefinition('twig.controller.preview_error');
}
});
$container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
}
}

View File

@ -12,9 +12,11 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\SecuredPageBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new SecuredPageBundle(),
new TestBundle(),
];

View File

@ -12,9 +12,11 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new EventBundle(),
new TestBundle(),
];

View File

@ -13,4 +13,5 @@ return [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AutowiringBundle\AutowiringBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -14,4 +14,5 @@ return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\CsrfFormLoginBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -13,4 +13,5 @@ return [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FirewallEntryPointBundle\FirewallEntryPointBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -13,4 +13,5 @@ return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -12,4 +12,5 @@
return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -11,8 +11,10 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new TestBundle(),
];

View File

@ -11,8 +11,10 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new TestBundle(),
];

View File

@ -12,9 +12,11 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\MissingUserProviderBundle\MissingUserProviderBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new MissingUserProviderBundle(),
new TestBundle(),
];

View File

@ -12,4 +12,5 @@
return [
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(),
];

View File

@ -11,8 +11,10 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new TestBundle(),
];

View File

@ -11,8 +11,10 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
new TestBundle(),
];

View File

@ -12,6 +12,7 @@
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\FormLoginBundle;
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
use Symfony\Bundle\TwigBundle\TwigBundle;
return [
@ -19,4 +20,5 @@ return [
new SecurityBundle(),
new TwigBundle(),
new FormLoginBundle(),
new TestBundle(),
];

View File

@ -19,7 +19,7 @@
"php": "^7.1.3",
"ext-xml": "*",
"symfony/config": "^4.2|^5.0",
"symfony/dependency-injection": "^4.2|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/http-kernel": "^4.4",
"symfony/security-core": "^4.4",
"symfony/security-csrf": "^4.2|^5.0",

View File

@ -133,9 +133,12 @@ abstract class AbstractRecursivePass implements CompilerPassInterface
list($class, $method) = $factory;
if ($class instanceof Reference) {
$class = $this->container->findDefinition((string) $class)->getClass();
} elseif ($class instanceof Definition) {
$class = $class->getClass();
} elseif (null === $class) {
$class = $definition->getClass();
}
if ('__construct' === $method) {
throw new RuntimeException(sprintf('Invalid service "%s": "__construct()" cannot be used as a factory method.', $this->currentId));
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException;
@ -79,27 +80,27 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass
/**
* @throws InvalidArgumentException When not enough parameters are defined for the method
*/
private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $configurationArguments): void
private function checkTypeDeclarations(Definition $checkedDefinition, \ReflectionFunctionAbstract $reflectionFunction, array $values): void
{
$numberOfRequiredParameters = $reflectionFunction->getNumberOfRequiredParameters();
if (\count($configurationArguments) < $numberOfRequiredParameters) {
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($configurationArguments)));
if (\count($values) < $numberOfRequiredParameters) {
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": "%s::%s()" requires %d arguments, %d passed.', $this->currentId, $reflectionFunction->class, $reflectionFunction->name, $numberOfRequiredParameters, \count($values)));
}
$reflectionParameters = $reflectionFunction->getParameters();
$checksCount = min($reflectionFunction->getNumberOfParameters(), \count($configurationArguments));
$checksCount = min($reflectionFunction->getNumberOfParameters(), \count($values));
for ($i = 0; $i < $checksCount; ++$i) {
if (!$reflectionParameters[$i]->hasType() || $reflectionParameters[$i]->isVariadic()) {
continue;
}
$this->checkType($checkedDefinition, $configurationArguments[$i], $reflectionParameters[$i]);
$this->checkType($checkedDefinition, $values[$i], $reflectionParameters[$i]);
}
if ($reflectionFunction->isVariadic() && ($lastParameter = end($reflectionParameters))->hasType()) {
$variadicParameters = \array_slice($configurationArguments, $lastParameter->getPosition());
$variadicParameters = \array_slice($values, $lastParameter->getPosition());
foreach ($variadicParameters as $variadicParameter) {
$this->checkType($checkedDefinition, $variadicParameter, $lastParameter);
@ -110,63 +111,82 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass
/**
* @throws InvalidParameterTypeException When a parameter is not compatible with the declared type
*/
private function checkType(Definition $checkedDefinition, $configurationArgument, \ReflectionParameter $parameter): void
private function checkType(Definition $checkedDefinition, $value, \ReflectionParameter $parameter): void
{
$parameterTypeName = $parameter->getType()->getName();
$type = $parameter->getType()->getName();
$referencedDefinition = $configurationArgument;
if ($referencedDefinition instanceof Reference) {
if (!$this->container->has($referencedDefinition)) {
if ($value instanceof Reference) {
if (!$this->container->has($value = (string) $value)) {
return;
}
$referencedDefinition = $this->container->findDefinition((string) $referencedDefinition);
if ('service_container' === $value && is_a($type, Container::class, true)) {
return;
}
$value = $this->container->findDefinition($value);
}
if ('self' === $parameterTypeName) {
$parameterTypeName = $parameter->getDeclaringClass()->getName();
}
if ('static' === $parameterTypeName) {
$parameterTypeName = $checkedDefinition->getClass();
if ('self' === $type) {
$type = $parameter->getDeclaringClass()->getName();
}
if ($referencedDefinition instanceof Definition) {
$class = $referencedDefinition->getClass();
if ('static' === $type) {
$type = $checkedDefinition->getClass();
}
if ($value instanceof Definition) {
$class = $value->getClass();
if (!$class || (!$this->autoload && !class_exists($class, false) && !interface_exists($class, false))) {
return;
}
if (!is_a($class, $parameterTypeName, true)) {
throw new InvalidParameterTypeException($this->currentId, $class, $parameter);
}
} else {
if (null === $configurationArgument && $parameter->allowsNull()) {
if ('callable' === $type && method_exists($class, '__invoke')) {
return;
}
if (\in_array($parameterTypeName, self::SCALAR_TYPES, true) && is_scalar($configurationArgument)) {
if ('iterable' === $type && is_subclass_of($class, 'Traversable')) {
return;
}
if ('iterable' === $parameterTypeName && $configurationArgument instanceof IteratorArgument) {
if (is_a($class, $type, true)) {
return;
}
if ('Traversable' === $parameterTypeName && $configurationArgument instanceof IteratorArgument) {
return;
}
throw new InvalidParameterTypeException($this->currentId, $class, $parameter);
}
if ($configurationArgument instanceof Parameter) {
return;
}
if ($value instanceof Parameter) {
$value = $this->container->getParameter($value);
} elseif (\is_string($value) && '%' === ($value[0] ?? '') && preg_match('/^%([^%]+)%$/', $value, $match)) {
$value = $this->container->getParameter($match[1]);
}
$checkFunction = sprintf('is_%s', $parameter->getType()->getName());
if (null === $value && $parameter->allowsNull()) {
return;
}
if (!$parameter->getType()->isBuiltin() || !$checkFunction($configurationArgument)) {
throw new InvalidParameterTypeException($this->currentId, \gettype($configurationArgument), $parameter);
}
if (\in_array($type, self::SCALAR_TYPES, true) && is_scalar($value)) {
return;
}
if ('callable' === $type && \is_array($value) && isset($value[0]) && ($value[0] instanceof Reference || $value[0] instanceof Definition)) {
return;
}
if ('iterable' === $type && (\is_array($value) || $value instanceof \Traversable || $value instanceof IteratorArgument)) {
return;
}
if ('Traversable' === $type && ($value instanceof \Traversable || $value instanceof IteratorArgument)) {
return;
}
$checkFunction = sprintf('is_%s', $parameter->getType()->getName());
if (!$parameter->getType()->isBuiltin() || !$checkFunction($value)) {
throw new InvalidParameterTypeException($this->currentId, \gettype($value), $parameter);
}
}
}