From 4fd391d445fc7a5197802cabb72070bcbb7074fe Mon Sep 17 00:00:00 2001 From: Farhad Safarov Date: Fri, 18 Jun 2021 10:59:41 +0300 Subject: [PATCH] container parameter types (#184) fixes https://github.com/psalm/psalm-plugin-symfony/issues/177 --- phpunit.xml | 28 +++---- psalm-baseline.xml | 7 +- src/Handler/ParameterBagHandler.php | 67 +++++++++++++++++ src/Plugin.php | 8 +- src/Symfony/ContainerMeta.php | 74 ++++++++++++++++++ .../acceptance/ParameterBag.feature | 75 +++++++++++++++++++ tests/acceptance/container.xml | 19 +++++ tests/unit/Symfony/ContainerMetaTest.php | 23 ++++++ 8 files changed, 285 insertions(+), 16 deletions(-) create mode 100644 src/Handler/ParameterBagHandler.php create mode 100644 tests/acceptance/acceptance/ParameterBag.feature diff --git a/phpunit.xml b/phpunit.xml index 3d075e6..8b17f78 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,21 +1,21 @@ - - - tests/unit - - - - - - src - - + verbose="true" +> + + + src + + + + + tests/unit + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 44f9642..30800ce 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + int @@ -11,4 +11,9 @@ NonInvariantDocblockPropertyType + + + attributes + + diff --git a/src/Handler/ParameterBagHandler.php b/src/Handler/ParameterBagHandler.php new file mode 100644 index 0000000..59edd36 --- /dev/null +++ b/src/Handler/ParameterBagHandler.php @@ -0,0 +1,67 @@ +args[0]->value) || !($expr->args[0]->value instanceof String_)) { + return; + } + + $argument = $expr->args[0]->value->value; + + // @todo find a better way to calculate return type + switch (gettype(self::$containerMeta->getParameter($argument))) { + case 'string': + $return_type_candidate = new Union([Atomic::create('string')]); + break; + case 'boolean': + $return_type_candidate = new Union([Atomic::create('bool')]); + break; + case 'integer': + $return_type_candidate = new Union([Atomic::create('integer')]); + break; + case 'double': + $return_type_candidate = new Union([Atomic::create('float')]); + break; + case 'array': + $return_type_candidate = new Union([Atomic::create('array')]); + break; + } + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 947335d..3a1eda3 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -14,6 +14,7 @@ use Psalm\SymfonyPsalmPlugin\Handler\ContainerHandler; use Psalm\SymfonyPsalmPlugin\Handler\DoctrineQueryBuilderHandler; use Psalm\SymfonyPsalmPlugin\Handler\DoctrineRepositoryHandler; use Psalm\SymfonyPsalmPlugin\Handler\HeaderBagHandler; +use Psalm\SymfonyPsalmPlugin\Handler\ParameterBagHandler; use Psalm\SymfonyPsalmPlugin\Handler\RequiredSetterHandler; use Psalm\SymfonyPsalmPlugin\Symfony\ContainerMeta; use Psalm\SymfonyPsalmPlugin\Twig\AnalyzedTemplatesTainter; @@ -84,7 +85,12 @@ class Plugin implements PluginEntryPointInterface } if (isset($config->containerXml)) { - ContainerHandler::init(new ContainerMeta((array) $config->containerXml)); + $containerMeta = new ContainerMeta((array) $config->containerXml); + ContainerHandler::init($containerMeta); + + require_once __DIR__.'/Handler/ParameterBagHandler.php'; + ParameterBagHandler::init($containerMeta); + $api->registerHooksFromClass(ParameterBagHandler::class); } $api->registerHooksFromClass(ContainerHandler::class); diff --git a/src/Symfony/ContainerMeta.php b/src/Symfony/ContainerMeta.php index 214f29b..e313336 100644 --- a/src/Symfony/ContainerMeta.php +++ b/src/Symfony/ContainerMeta.php @@ -19,6 +19,11 @@ class ContainerMeta */ private $classNames = []; + /** + * @var array + */ + private $parameters = []; + public function __construct(array $containerXmlPaths) { $this->init($containerXmlPaths); @@ -43,6 +48,18 @@ class ContainerMeta $this->services[$service->getId()] = $service; } + /** + * @return mixed|null + */ + public function getParameter(string $key) + { + if (isset($this->parameters[$key])) { + return $this->parameters[$key]; + } + + return null; + } + /** * @return array */ @@ -90,9 +107,66 @@ class ContainerMeta $this->add($service); } + /** @var \SimpleXMLElement $parameter */ + foreach ($xml->parameters->parameter as $parameter) { + $value = $this->getXmlParameterValue($parameter); + + $attributes = $parameter->attributes(); + if (!isset($attributes->key)) { + continue; + } + + $this->parameters[(string) $attributes->key] = $value; + } + return; } throw new ConfigException('Container xml file(s) not found at '); } + + /** + * @return mixed + */ + private function getXmlParameterValue(\SimpleXMLElement $parameter) + { + $value = null; + $attributes = $parameter->attributes(); + if (isset($attributes->type)) { + switch ((string) $attributes->type) { + case 'binary': + $value = base64_decode((string) $parameter, true); + break; + case 'collection': + foreach ($parameter->children() as $child) { + $childAttributes = $child->attributes(); + if (isset($childAttributes->key)) { + $value[(string) $childAttributes->key] = $this->getXmlParameterValue($child); + } else { + $value[] = $this->getXmlParameterValue($child); + } + } + break; + case 'string': + default: + $value = (string) $parameter; + break; + } + } else { + $value = (string) $parameter; + if ('true' === $value || 'false' === $value) { + $value = (bool) $value; + } elseif ('null' === $value) { + $value = null; + } elseif (is_numeric($value)) { + if (false === strpos($value, '.')) { + $value = (int) $value; + } else { + $value = (float) $value; + } + } + } + + return $value; + } } diff --git a/tests/acceptance/acceptance/ParameterBag.feature b/tests/acceptance/acceptance/ParameterBag.feature new file mode 100644 index 0000000..7a9f8c9 --- /dev/null +++ b/tests/acceptance/acceptance/ParameterBag.feature @@ -0,0 +1,75 @@ +@symfony-common +Feature: ParameterBag + + Background: + Given I have Symfony plugin enabled with the following config + """ + ../../tests/acceptance/container.xml + """ + And I have the following code preamble + """ + get('kernel.environment'); + + /** @psalm-trace $debugEnabled */ + $debugEnabled = $parameterBag->get('debug_enabled'); + + /** @psalm-trace $debugDisabled */ + $debugDisabled = $parameterBag->get('debug_disabled'); + + /** @psalm-trace $version */ + $version = $parameterBag->get('version'); + + /** @psalm-trace $integerOne */ + $integerOne = $parameterBag->get('integer_one'); + + /** @psalm-trace $pi */ + $pi = $parameterBag->get('pi'); + + /** @psalm-trace $collection1 */ + $collection1 = $parameterBag->get('collection1'); + } + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | Trace | $kernelEnvironment: string | + | Trace | $debugEnabled: bool | + | Trace | $debugDisabled: bool | + | Trace | $version: string | + | Trace | $integerOne: int | + | Trace | $pi: float | + | Trace | $collection1: array | + And I see no other errors + + Scenario: Get non-existent parameter + Given I have the following code + """ + class Foo + { + public function __invoke(ParameterBagInterface $parameterBag) + { + /** @psalm-trace $nonExistentParameter */ + $nonExistentParameter = $parameterBag->get('non_existent_parameter'); + } + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | MixedAssignment | Unable to determine the type that $nonExistentParameter is being assigned to | + | Trace | $nonExistentParameter: mixed | + And I see no other errors diff --git a/tests/acceptance/container.xml b/tests/acceptance/container.xml index fb8b869..ff0b677 100644 --- a/tests/acceptance/container.xml +++ b/tests/acceptance/container.xml @@ -2,6 +2,25 @@ dev + true + false + 1 + 1 + 3.14 + + val1 + val2 + + + val + + true + 2.18 + + something + + + diff --git a/tests/unit/Symfony/ContainerMetaTest.php b/tests/unit/Symfony/ContainerMetaTest.php index f0378ea..af18ff0 100644 --- a/tests/unit/Symfony/ContainerMetaTest.php +++ b/tests/unit/Symfony/ContainerMetaTest.php @@ -138,4 +138,27 @@ class ContainerMetaTest extends TestCase $service = $containerMeta->get('service_container'); $this->assertSame('Symfony\Component\DependencyInjection\ContainerInterface', $service->getClassName()); } + + public function testGetParameter(): void + { + $this->assertSame('dev', $this->containerMeta->getParameter('kernel.environment')); + $this->assertSame(true, $this->containerMeta->getParameter('debug_enabled')); + $this->assertSame('1', $this->containerMeta->getParameter('version')); + $this->assertSame(1, $this->containerMeta->getParameter('integer_one')); + $this->assertSame(3.14, $this->containerMeta->getParameter('pi')); + $this->assertSame([ + 'key1' => 'val1', + 'key2' => 'val2', + ], $this->containerMeta->getParameter('collection1')); + $this->assertSame([ + 'key' => 'val', + 'child_collection' => [ + 'boolean' => true, + 'float' => 2.18, + 'grandchild_collection' => [ + 'string' => 'something', + ], + ] + ], $this->containerMeta->getParameter('nested_collection')); + } }